feat: refactor dialog components to use light-bottomsheet for improved UX

- Replaced SharedDialogComponent with SharedLightBottomsheetComponent in customer-dialog, order-submitted-dialog, payment form-dialog, and pre-invoice-dialog components.
- Updated styles and properties for light-bottomsheet to enhance responsiveness and usability.
- Modified payload form components to use measure units instead of unit types for better clarity.
- Adjusted form controls to use consistent naming conventions for amounts and percentages.
- Enhanced support for overlay options in select components to manage z-index dynamically.
- Improved support for RTL layouts in input groups and other components.
This commit is contained in:
2026-05-13 13:44:33 +03:30
parent dba960c454
commit 42b8476b96
39 changed files with 396 additions and 143 deletions
@@ -1,6 +1,5 @@
import ISummary from '@/core/models/summary';
import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/landing/models';
import { UnitType } from '@/utils';
export interface ISalesInvoicesRawResponse {
id: string;
@@ -29,7 +28,7 @@ interface Payment {
}
interface Item {
unit_type: UnitType;
measure_unit: ISummary;
discount: string;
quantity: string;
total_amount: string;
@@ -46,6 +45,6 @@ interface Good {
barcode: null;
local_sku: null;
pricing_model: string;
unit_type: string;
measure_unit: ISummary;
category: ISummary;
}
@@ -31,14 +31,14 @@
}
</ul>
</div>
<div class="mt-auto">
<hr class="mb-4 mx-4 border-t border-0 border-surface" />
@if (isPwaBuild) {
@if (isPwaBuild) {
<div class="mt-auto">
<hr class="mb-4 mx-4 border-t border-0 border-surface" />
<div class="px-6 pb-6 text-sm text-muted-color">
نسخه برنامه : <span class="font-semibold">{{ appVersion }}</span>
</div>
}
</div>
</div>
}
</div>
</ng-template>
</p-drawer>
@@ -53,7 +53,7 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
icon: 'pi pi-fw pi-cog',
},
{
label: 'درباره ما',
label: 'درباره نرم‌افزار',
routerLink: posAboutNamedRoutes.about.meta.pagePath!(),
icon: 'pi pi-fw pi-info-circle',
},
@@ -2,15 +2,7 @@ import { AuthService } from '@/core';
import { LayoutService } from '@/layout/service/layout.service';
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
Component,
TemplateRef,
ViewChild,
computed,
inject,
signal,
} from '@angular/core';
import { Component, TemplateRef, ViewChild, computed, inject, signal } from '@angular/core';
import { RouterLinkWithHref, RouterOutlet } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { Button, ButtonDirective } from 'primeng/button';
@@ -38,7 +30,7 @@ import { PosChooseCardsComponent } from './choose-pos.component';
RouterOutlet,
],
})
export class PosPagesLayoutComponent implements AfterViewInit {
export class PosPagesLayoutComponent {
constructor() {}
// private readonly nativeBridgeService = inject(NativeBridgeService);
@@ -118,9 +110,6 @@ export class PosPagesLayoutComponent implements AfterViewInit {
ngOnInit() {
this.layoutService.changeIsFullPage(true);
}
ngAfterViewInit() {
this.layoutService.setTopbarStartSlot(this.topbarStart);
this.layoutService.setTopbarCenterSlot(this.topbarCenter);
this.layoutService.setTopbarEndSlot(this.topbarEnd);
+1 -2
View File
@@ -1,5 +1,4 @@
import ISummary from '@/core/models/summary';
import { UnitType } from '@/utils';
export interface IGoodRawResponse {
id: string;
@@ -10,7 +9,7 @@ export interface IGoodRawResponse {
name: string;
};
category: ISummary;
unit_type: UnitType;
measure_unit: ISummary;
pricing_model: string;
description?: string;
image_url: string;
@@ -1,6 +1,55 @@
<div class="w-full h-full min-h-[60svh] flex items-center justify-center p-4">
<div class="text-center">
<h2 class="text-xl font-semibold mb-2">درباره سیستم</h2>
<p class="text-muted-color">این صفحه به زودی تکمیل می شود.</p>
</div>
<div class="w-full min-h-[60svh] p-4 md:p-6">
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<div class="bg-gradient-to-l from-emerald-50 via-teal-50 to-cyan-50 p-5 md:p-7">
<p class="mb-2 inline-flex items-center rounded-full bg-white px-3 py-1 text-xs font-semibold text-emerald-700">
{{ brandingInfo.appTitle }}
</p>
<span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">درباره نسخه تیس</span>
<p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base">
این نسخه برای استفاده روزانه روی دستگاه های PSP بهینه شده است تا عملیات ثبت و صدور صورتحساب‌‌های مالیاتی با سرعت
بالاتر، پایداری بیشتر و تجربه کاربری ساده تر انجام شود.
</p>
</div>
<div class="grid gap-3 p-4 md:grid-cols-3 md:gap-4 md:p-6">
<!-- <article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<h2 class="mb-2 text-sm font-bold text-surface-900">پایداری در وب ویو</h2>
<p class="text-sm leading-6 text-surface-700">
رفتار رابط کاربری برای محیط WebView تنظیم شده تا روی سخت افزارهای ضعیف تر هم اجرای روان تری داشته باشد.
</p>
</article> -->
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">تمرکز بر سرعت عملیات</span>
<p class="text-sm leading-6 text-surface-700">
مسیرهای کلیدی مانند انتخاب کالا، ورود مقادیر و ثبت نهایی برای کاهش تاخیر و افزایش سرعت کاربر بهینه شده اند.
</p>
</article>
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<span class="block mb-4 text-xl font-bold text-surface-900">سازگار با سناریوهای فروش</span>
<p class="text-sm leading-6 text-surface-700">
طراحی این نسخه بر مبنای سناریوهای واقعی فروشگاه انجام شده تا اپراتور با کمترین کلیک فرآیند را تکمیل کند.
</p>
</article>
</div>
<div class="mx-4 mb-4 rounded-xl border border-surface-200 bg-surface-50 p-4 md:mx-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="text-sm text-surface-700">
نسخه برنامه:
<span class="font-semibold text-surface-900">{{ appVersion() }}</span>
</div>
<button pButton type="button" [outlined]="!hasUpdate()" [disabled]="isCheckingUpdate()" (click)="onUpdateCtaClick()">
{{ updateCtaLabel() }}
</button>
</div>
</div>
<div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6">
<p class="text-xs leading-6 text-surface-600 md:text-sm">
در صورت نیاز به راهنمایی بیشتر، به صفحه‌ی راهنمای استفاده مراجعه کنید یا با پشتیبانی تماس بگیرید.
</p>
</div>
</section>
</div>
@@ -1,7 +1,87 @@
import { Component } from '@angular/core';
import { brandingConfig } from '@/branding/branding.config';
import { Component, computed, inject, signal } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { ButtonDirective } from 'primeng/button';
import { filter } from 'rxjs';
@Component({
selector: 'pos-about-page',
templateUrl: './root.component.html',
imports: [ButtonDirective],
})
export class PosAboutPageComponent {}
export class PosAboutPageComponent {
private readonly swUpdate = inject(SwUpdate);
brandingInfo = brandingConfig;
appVersion = signal('0.0.0');
isPwaBuild = this.swUpdate.isEnabled;
isCheckingUpdate = signal(false);
hasUpdate = signal(false);
isUpdated = signal(false);
updateCtaLabel = computed(() => {
if (!this.isPwaBuild) return 'به روزرسانی فقط در نسخه PWA';
if (this.isUpdated()) return 'به روز است';
if (this.hasUpdate()) return 'به روزرسانی برنامه';
if (this.isCheckingUpdate()) return 'در حال بررسی...';
return 'بررسی به روزرسانی';
});
ngOnInit() {
if (!this.isPwaBuild) return;
this.loadVersion();
this.swUpdate.versionUpdates
.pipe(filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'))
.subscribe((event) => {
const appData = event.latestVersion.appData as { appVersion?: string } | undefined;
this.appVersion.set(appData?.appVersion || this.appVersion());
this.hasUpdate.set(true);
this.isUpdated.set(false);
});
}
async onUpdateCtaClick() {
if (!this.isPwaBuild || this.isCheckingUpdate()) return;
if (this.hasUpdate()) {
await this.applyUpdate();
return;
}
await this.checkForUpdate();
}
private async checkForUpdate() {
this.isCheckingUpdate.set(true);
this.isUpdated.set(false);
try {
const hasUpdate = await this.swUpdate.checkForUpdate();
this.hasUpdate.set(hasUpdate);
this.isUpdated.set(!hasUpdate);
} finally {
this.isCheckingUpdate.set(false);
}
}
private async applyUpdate() {
this.isCheckingUpdate.set(true);
try {
await this.swUpdate.activateUpdate();
this.isUpdated.set(true);
this.hasUpdate.set(false);
window.location.reload();
} finally {
this.isCheckingUpdate.set(false);
}
}
private loadVersion() {
fetch('/ngsw.json')
.then((res) => res.json())
.then((data: { appData?: { appVersion?: string } }) => {
this.appVersion.set(data?.appData?.appVersion || this.appVersion());
})
.catch(() => {});
}
}
@@ -1,4 +1,9 @@
<shared-dialog [(visible)]="visible" header="اطلاعات مشتری" [modal]="true" [style]="{ width: '560px' }">
<shared-light-bottomsheet
[(visible)]="visible"
header="اطلاعات مشتری"
[modal]="true"
[style]="{ 'max-height': '90svh', height: 'auto' }"
>
<div class="card p-2! flex justify-center">
<p-selectbutton [options]="customerTypes" [(ngModel)]="selectedCustomerType" optionValue="value">
<ng-template #item let-item>
@@ -13,4 +18,4 @@
} @else if (selectedCustomerType() === "LEGAL") {
<pos-customer-legal-form (onSubmit)="onSubmitCustomer()" (onClose)="close()" />
}
</shared-dialog>
</shared-light-bottomsheet>
@@ -1,5 +1,5 @@
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { Component, EventEmitter, inject, Output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@@ -13,7 +13,7 @@ import { CustomerUnknownFormComponent } from './unknown/form.component';
selector: 'pos-order-customer-dialog',
templateUrl: './customer-dialog.component.html',
imports: [
SharedDialogComponent,
SharedLightBottomsheetComponent,
CustomerIndividualFormComponent,
FormsModule,
CustomerLegalFormComponent,
@@ -16,7 +16,7 @@
</span>
</div>
<span class="text-sm text-ellipsis text-nowrap overflow-hidden">
{{ inOrderGood.quantity }} {{ enumTranslator(inOrderGood.good.unit_type) }}
{{ inOrderGood.quantity }} {{ inOrderGood.good.measure_unit.name }}
</span>
</div>
@if (!inOrderGood.discount_amount) {
@@ -53,9 +53,11 @@
</div>
</div>
<pos-payload-form-dialog
[(visible)]="visible"
[good]="inOrderGood.good"
[initialValues]="inOrderGood"
[orderItemId]="inOrderGood.id"
/>
@if (visible()) {
<pos-payload-form-dialog
[(visible)]="visible"
[good]="inOrderGood.good"
[initialValues]="inOrderGood"
[orderItemId]="inOrderGood.id"
/>
}
@@ -1,8 +1,8 @@
<shared-dialog
<shared-light-bottomsheet
[(visible)]="visible"
header="ثبت موفقیت‌آمیز"
[modal]="true"
[style]="{ width: '420px' }"
[style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true"
(onHide)="close()"
>
@@ -15,4 +15,4 @@
<button pButton type="button" outlined (click)="sendInvoice()">ارسال به سامانه مودیان</button>
<button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button>
</div>
</shared-dialog>
</shared-light-bottomsheet>
@@ -2,7 +2,7 @@ import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service';
import { PosInfoStore } from '@/domains/pos/store';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedDialogComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { formatJalali } from '@/utils';
import { Component, computed, inject, Input, signal } from '@angular/core';
import { Router } from '@angular/router';
@@ -15,7 +15,7 @@ import { IPosOrderResponse } from '../../models';
@Component({
selector: 'pos-order-submitted-dialog',
templateUrl: 'order-submitted-dialog.component.html',
imports: [SharedDialogComponent, ButtonDirective],
imports: [SharedLightBottomsheetComponent, ButtonDirective],
})
export class PosOrderSubmittedDialogComponent extends AbstractDialog {
@Input({ required: true }) invoice!: IPosOrderResponse;
@@ -12,7 +12,7 @@
} @else if (isStandardMode()) {
<pos-standard-payload-form
[initialValues]="standardPayload()"
[unitType]="good.unit_type"
[measureUnit]="good.measure_unit"
[editMode]="editMode()"
(onSubmit)="submit($event)"
/>
@@ -6,7 +6,7 @@
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.wages_percentage"
[amountControl]="form.controls.payload.controls.wages_amount"
[amountControl]="form.controls.payload.controls.wages"
[baseAmount]="unitWithQuantity()"
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
@@ -15,7 +15,7 @@
/>
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.commission_percentage"
[amountControl]="form.controls.payload.controls.commission_amount"
[amountControl]="form.controls.payload.controls.commission"
[baseAmount]="unitWithQuantity()"
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
@@ -24,7 +24,7 @@
/>
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.profit_percentage"
[amountControl]="form.controls.payload.controls.profit_amount"
[amountControl]="form.controls.payload.controls.profit"
[baseAmount]="totalAmountBeforeProfit()"
[minAmount]="0"
[maxAmount]="totalAmountBeforeProfit()"
@@ -35,7 +35,7 @@
<app-amount-percentage-input
[percentageControl]="form.controls.discount_percentage"
[amountControl]="form.controls.discount_amount"
[amountControl]="form.controls.discount"
[baseAmount]="baseAmountForDiscountCalculation()"
[minAmount]="0"
[maxAmount]="baseAmountForDiscountCalculation()"
@@ -59,7 +59,7 @@
<pos-form-dialog-amount-card-template
[totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()"
[discountAmount]="form.controls.discount_amount.value || 0"
[discountAmount]="form.controls.discount.value || 0"
[taxAmount]="taxAmount()"
/>
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
@@ -31,6 +31,17 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
IPosOrderItem<IGoldPayload>,
IPosOrderItem<IGoldPayload>
> {
override defaultValues: Partial<IPosOrderItem<IGoldPayload>> = {
unit_price: 200_000_000,
quantity: 2,
payload: {
commission: 10_000,
wages: 10_000,
profit: 10_000,
karat: '18' as TGoldKarat,
} as any,
};
@Output() onSubmitForm = new EventEmitter<IGoldPayload>();
@Output() onChangeTotalAmount = new EventEmitter<number>();
@@ -59,14 +70,14 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
const form = this.fb.group({
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
discount_amount: [this.initialValues?.discount || 0],
discount: [this.initialValues?.discount || 0],
discount_percentage: [0, [Validators.max(100), Validators.min(0)]],
payload: this.fb.group({
commission_amount: [this.initialValues?.payload?.commission || 0, [Validators.required]],
commission: [this.initialValues?.payload?.commission || 0, [Validators.required]],
commission_percentage: [0, [Validators.required, Validators.max(100), Validators.min(0)]],
wages_amount: [this.initialValues?.payload?.wages || 0, [Validators.required]],
wages: [this.initialValues?.payload?.wages || 0, [Validators.required]],
wages_percentage: [0, [Validators.required, Validators.max(100), Validators.min(0)]],
profit_amount: [this.initialValues?.payload?.profit || 0, [Validators.required]],
profit: [this.initialValues?.payload?.profit || 0, [Validators.required]],
profit_percentage: [0, [Validators.required, Validators.max(100), Validators.min(0)]],
karat: [this.initialValues?.payload?.karat || '', [Validators.required]],
}),
@@ -76,17 +87,15 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
this.updateCalculateAmount(value as any);
});
form.controls.payload.controls.profit_amount.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(() => {
// if ((form.controls.discount_amount.value || 0) > (value || 0)) {
form.controls.discount_amount.setValue(0, { emitEvent: false });
// }
});
form.controls.payload.controls.profit.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
// if ((form.controls.discount_amount.value || 0) > (value || 0)) {
form.controls.discount.setValue(0, { emitEvent: false });
// }
});
form.controls.payload.controls.profit_percentage.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(() => {
form.controls.discount_amount.setValue(0, { emitEvent: false });
form.controls.discount.setValue(0, { emitEvent: false });
});
return form;
@@ -101,19 +110,19 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
base_total_amount: this.baseTotalAmount(),
tax_amount: this.taxAmount(),
payload: {
commission: this.form.controls.payload.controls.commission_amount.value || 0,
commission: this.form.controls.payload.controls.commission.value || 0,
karat: this.form.controls.payload.controls.karat.value as TGoldKarat,
profit: this.form.controls.payload.controls.profit_amount.value || 0,
wages: this.form.controls.payload.controls.wages_amount.value || 0,
profit: this.form.controls.payload.controls.profit.value || 0,
wages: this.form.controls.payload.controls.wages.value || 0,
},
});
}
updateCalculateAmount(payload: Partial<IPosOrderItem<any>>) {
const commissionAmount = Number(payload.payload?.commission_amount ?? '0');
const wageAmount = Number(payload.payload?.wages_amount ?? '0');
const discountAmount = Number(payload.discount_amount ?? '0');
const profitAmount = Number(payload.payload?.profit_amount ?? '0');
const commissionAmount = Number(payload.payload?.commission ?? '0');
const wageAmount = Number(payload.payload?.wages ?? '0');
const discountAmount = Number(payload.discount ?? '0');
const profitAmount = Number(payload.payload?.profit ?? '0');
const unitPrice = Number(payload.unit_price ?? '0');
const quantity = Number(payload.quantity ?? '0');
@@ -3,9 +3,9 @@
<app-input
[control]="form.controls.quantity"
name="quantity"
[label]="preparedFormVisuals().quantityLabel"
[label]="measureUnit.name"
type="number"
[suffix]="preparedFormVisuals().quantitySymbolText"
[suffix]="measureUnit.name"
[min]="0"
/>
@@ -1,7 +1,8 @@
import ISummary from '@/core/models/summary';
import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components';
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
import { formatWithCurrency, getGoodUnitTypeProperties, UnitType } from '@/utils';
import { formatWithCurrency } from '@/utils';
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, Validators } from '@angular/forms';
@@ -24,7 +25,7 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
IPosOrderItem<IStandardPayload>,
IPosOrderItem<IStandardPayload>
> {
@Input({ required: true }) unitType!: UnitType;
@Input({ required: true }) measureUnit!: ISummary;
@Output() onChangeTotalAmount = new EventEmitter<number>();
private readonly initialForm = () => {
@@ -53,8 +54,6 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
});
}
preparedFormVisuals = computed(() => getGoodUnitTypeProperties(this.unitType));
baseTotalAmount = signal<number>(0);
totalAmount = signal<number>(0);
discountAmount = signal<number>(0);
@@ -1,8 +1,8 @@
<shared-dialog
<shared-light-bottomsheet
[(visible)]="visible"
header="اطلاعات پرداخت"
[modal]="true"
[style]="{ width: '500px' }"
[style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true"
(onHide)="close()"
>
@@ -71,4 +71,4 @@
/>
</form>
</div>
</shared-dialog>
</shared-light-bottomsheet>
@@ -3,7 +3,7 @@ import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service';
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent, KeyValueComponent } from '@/shared/components';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFieldComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core';
@@ -27,7 +27,7 @@ import { PosLandingStore } from '../../store/main.store';
FormFooterActionsComponent,
KeyValueComponent,
ButtonDirective,
SharedDialogComponent,
SharedLightBottomsheetComponent,
Select,
UikitFieldComponent,
],
@@ -1,11 +1,11 @@
<shared-dialog
<shared-light-bottomsheet
[(visible)]="visible"
header="پیش فاکتور"
[modal]="true"
[style]="{ width: '500px' }"
[style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true"
(onHide)="close()"
>
<form [formGroup]="form"></form>
<uikit-datepicker [control]="form.controls.invoiceDate" label="تاریخ فاکتور" />
</shared-dialog>
</shared-light-bottomsheet>
@@ -1,14 +1,14 @@
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { PosLandingStore } from '../store/main.store';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
@Component({
selector: 'pos-pre-invoice-dialog',
templateUrl: './pre-invoice-dialog.component.html',
imports: [SharedDialogComponent, UikitFlatpickrJalaliComponent, ReactiveFormsModule],
imports: [SharedLightBottomsheetComponent, UikitFlatpickrJalaliComponent, ReactiveFormsModule],
})
export class PosPreInvoiceDialogComponent extends AbstractDialog {
private readonly store = inject(PosLandingStore);
@@ -1,5 +1,5 @@
import ISummary from '@/core/models/summary';
import { IGoodResponse } from '@/domains/pos/models/good.io';
import { UnitType } from '@/utils';
import { TPosOrderGoodPayload } from './payload';
export interface IPosOrderItem<T = TPosOrderGoodPayload> {
@@ -7,7 +7,7 @@ export interface IPosOrderItem<T = TPosOrderGoodPayload> {
quantity: number;
total_amount: number;
discount_amount: number;
unit_type: UnitType;
measure_unit: ISummary;
good_id?: string;
service_id?: string;
notes?: string;
@@ -221,7 +221,7 @@ export class PosLandingStore {
items: this.inOrderGoods().map(({ good, ...rest }) => ({
...rest,
good_id: good.id,
unit_type: good.unit_type,
measure_unit: good.measure_unit,
})),
invoice_date: new Date().toISOString(),
payments: this.payments(),
@@ -34,17 +34,11 @@
</div>
</button>
}
<shared-dialog
[(visible)]="showInvoiceBottomSheet"
position="bottom"
[modal]="true"
[closable]="true"
header="فاکتور"
>
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [modal]="true" [closable]="true" header="فاکتور">
@if (showInvoiceBottomSheet()) {
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSuccess)="onSuccessSubmit($event)" />
}
</shared-dialog>
</shared-light-bottomsheet>
@if (responseInvoice()) {
<pos-order-submitted-dialog [(visible)]="isVisibleSuccessDialog" [invoice]="responseInvoice()!" />
@@ -2,7 +2,7 @@
import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store/pos.store';
import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-mobile';
import { SharedDialogComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { PriceMaskDirective } from '@/shared/directives';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
@@ -24,7 +24,7 @@ import { PosLandingStore } from '../store/main.store';
PosOrderSectionComponent,
PageLoadingComponent,
PriceMaskDirective,
SharedDialogComponent,
SharedLightBottomsheetComponent,
ButtonDirective,
PosOrderSubmittedDialogComponent,
],
@@ -1,6 +1,47 @@
<div class="w-full h-full min-h-[60svh] flex items-center justify-center p-4">
<div class="text-center">
<h2 class="text-xl font-semibold mb-2">پشتیبانی</h2>
<p class="text-muted-color">این صفحه به زودی تکمیل می شود.</p>
</div>
<div class="w-full min-h-[60svh] p-4 md:p-6">
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<div class="bg-gradient-to-l from-sky-50 via-cyan-50 to-teal-50 p-5 md:p-7">
<h1 class="text-2xl font-bold text-surface-900 md:text-3xl">پشتیبانی تیس</h1>
<p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base">
در صورت بروز مشکل در ثبت سفارش، صدور صورتحساب یا تنظیمات دستگاه، تیم پشتیبانی آماده پاسخگویی است. لطفا هنگام
تماس، نام پایانه و شماره سریال دستگاه را همراه داشته باشید.
</p>
</div>
<div class="grid gap-3 p-4 md:grid-cols-2 md:gap-4 md:p-6">
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<h2 class="mb-3 text-sm font-bold text-surface-900">شماره های تماس مستقیم</h2>
<ul class="space-y-2 text-sm text-surface-700">
<li class="flex items-center justify-between gap-2">
<span>واحد پشتیبانی 1</span>
<a class="font-semibold text-primary" href="tel:02191012345" dir="ltr">021-91012345</a>
</li>
<li class="flex items-center justify-between gap-2">
<span>واحد پشتیبانی 2</span>
<a class="font-semibold text-primary" href="tel:02191012346" dir="ltr">021-91012346</a>
</li>
<li class="flex items-center justify-between gap-2">
<span>پاسخگویی اضطراری</span>
<a class="font-semibold text-primary" href="tel:09121234567" dir="ltr">0912-123-4567</a>
</li>
</ul>
</article>
<article class="rounded-xl border border-surface-200 bg-surface-50 p-4">
<h2 class="mb-3 text-sm font-bold text-surface-900">راهنمای سریع قبل از تماس</h2>
<ul class="space-y-2 text-sm leading-6 text-surface-700">
<li>اتصال اینترنت دستگاه را بررسی کنید.</li>
<li>یک بار برنامه را ببندید و دوباره اجرا کنید.</li>
<li>در صفحه درباره نرم افزار، نسخه برنامه را اعلام کنید.</li>
<li>در صورت خطا، متن خطا یا تصویر آن را برای پشتیبانی ارسال کنید.</li>
</ul>
</article>
</div>
<div class="border-t border-surface-200 bg-surface-50 px-4 py-4 md:px-6">
<p class="text-xs leading-6 text-surface-600 md:text-sm">
ساعات پاسخگویی: شنبه تا چهارشنبه 8:30 تا 17:30 و پنجشنبه 8:30 تا 13:00
</p>
</div>
</section>
</div>
@@ -38,6 +38,11 @@ export class AppLayout {
public renderer: Renderer2,
public router: Router,
) {
this.layoutService.headerSlot$.subscribe((tpl) => (this.topBarMoreAction = tpl));
this.layoutService.topbarStartSlot$.subscribe((tpl) => (this.topBarStartAction = tpl));
this.layoutService.topbarCenterSlot$.subscribe((tpl) => (this.topBarCenterAction = tpl));
this.layoutService.topbarEndSlot$.subscribe((tpl) => (this.topBarEndAction = tpl));
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
if (!this.menuOutsideClickListener) {
this.menuOutsideClickListener = this.renderer.listen('document', 'click', (event) => {
@@ -133,11 +138,6 @@ export class AppLayout {
});
ngOnInit() {
this.layoutService.headerSlot$.subscribe((tpl) => (this.topBarMoreAction = tpl));
this.layoutService.topbarStartSlot$.subscribe((tpl) => (this.topBarStartAction = tpl));
this.layoutService.topbarCenterSlot$.subscribe((tpl) => (this.topBarCenterAction = tpl));
this.layoutService.topbarEndSlot$.subscribe((tpl) => (this.topBarEndAction = tpl));
if (window.location.pathname === '/') {
if (!this.authService.currentAccount()) {
this.router.navigateByUrl('auth');
@@ -37,14 +37,14 @@ export abstract class AbstractForm<
}
ngOnInit() {
this.form.reset(this.initialValues ?? this.defaultValues);
this.form.patchValue(this.initialValues ?? this.defaultValues);
// Object.entries(this.form.controls).forEach(([key, control]) => {
// console.log(key, control);
// });
}
ngOnChanges() {
this.form.reset(this.initialValues ?? this.defaultValues);
this.form.patchValue(this.initialValues ?? this.defaultValues);
}
onSuccess(response: Response): void {}
@@ -7,6 +7,7 @@
[formControl]="control"
[showClear]="showClear"
[name]="name || type"
[overlayOptions]="overlayOptions()"
appendTo="body"
/>
</uikit-field>
+21 -1
View File
@@ -1,5 +1,6 @@
import { UikitFieldComponent } from '@/uikit';
import { Component, computed, inject, Input } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Component, Inject, computed, inject, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { Observable } from 'rxjs';
@@ -19,6 +20,9 @@ export class EnumSelectComponent extends AbstractSelectComponent<string, IApiEnu
@Input() customLabel?: string;
private readonly service = inject(EnumsService);
constructor(@Inject(DOCUMENT) private readonly document: Document) {
super();
}
label = computed(() => this.customLabel ?? apiEnumTranslates[this.type]);
@@ -29,4 +33,20 @@ export class EnumSelectComponent extends AbstractSelectComponent<string, IApiEnu
ngOnInit() {
this.getData();
}
overlayOptions() {
return {
autoZIndex: true,
baseZIndex: this.overlayBaseZIndex(),
};
}
overlayBaseZIndex() {
const drawers = Array.from(this.document.body.querySelectorAll('.p-drawer,[data-pc-name="drawer"]')) as HTMLElement[];
const zIndexes = drawers
.map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10))
.filter((zIndex) => Number.isFinite(zIndex) && zIndex > 0);
return zIndexes.length ? Math.max(...zIndexes) + 2 : 1000;
}
}
@@ -24,8 +24,8 @@
[attr.name]="name"
[attr.placeholder]="placeholder"
[attr.autocomplete]="autocomplete"
[class]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
dir="ltr"
[inputStyle]="{ direction: 'ltr' }"
[inputStyleClass]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
[required]="required"
[invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)"
(onInput)="onPriceInput($event)"
@@ -58,9 +58,10 @@
<p-select
[formControl]="selectedType"
[options]="typeSelectOptions"
tabindex="-1"
optionValue="value"
size="small"
class="border-0!"
class="border-none! shadow-none! w-full!"
/>
</p-inputgroup-addon>
</p-inputgroup>
@@ -180,11 +180,16 @@ export class AmountPercentageInputComponent {
const amountValue = Number(this.amountControl.value || 0);
const percentageValue = Number(this.percentageControl.value || 0);
const isPercentageType = percentageValue > 0 && amountValue <= 0;
console.log(amountValue, percentageValue, isPercentageType);
this.preparedLabel.set(this.prepareLabel(isPercentageType));
this.selectedType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
this.preparedLabel.set(this.prepareLabel(value === 'percentage'));
});
this.selectedType.setValue(isPercentageType ? 'percentage' : 'amount');
}
ngOnChanges() {
@@ -1,6 +1,6 @@
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<p-checkBox [formControl]="control" [name]="name" [id]="name" binary class="shrink-0"> </p-checkBox>
<p-checkBox [formControl]="control" [name]="name" [inputId]="name" binary class="shrink-0"> </p-checkBox>
<uikit-label [name]="name"> {{ label }} </uikit-label>
</div>
@@ -1,20 +1,21 @@
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div>
<section
class="light-bottomsheet-panel"
role="dialog"
aria-modal="true"
[attr.aria-hidden]="!visible"
[ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }"
>
<section class="light-bottomsheet-panel" [ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }">
<div class="light-bottomsheet-content">
@if (header || closable) {
<header class="light-bottomsheet-header">
<h3 class="light-bottomsheet-title">{{ header }}</h3>
<span class="text-xl font-bold">{{ header }}</span>
@if (closable) {
<button type="button" class="light-bottomsheet-close" (click)="onVisibilityChange(false)" aria-label="Close">
×
</button>
<p-button
type="button"
icon="pi pi-times"
text
size="small"
class="light-bottomsheet-close"
(click)="onVisibilityChange(false)"
aria-label="Close"
>
</p-button>
}
</header>
}
@@ -1,27 +1,29 @@
import { NgStyle } from '@angular/common';
import { DOCUMENT } from '@angular/common';
import { DOCUMENT, NgStyle } from '@angular/common';
import {
Component,
ElementRef,
EventEmitter,
Inject,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges,
} from '@angular/core';
import { Button } from 'primeng/button';
@Component({
selector: 'shared-light-bottomsheet',
templateUrl: './light-bottomsheet.component.html',
imports: [NgStyle],
imports: [NgStyle, Button],
styles: [
`
:host {
position: fixed;
inset: 0;
z-index: 11020;
z-index: 1210;
}
.light-bottomsheet-mask {
@@ -66,8 +68,6 @@ import {
.light-bottomsheet-title {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
}
.light-bottomsheet-close {
@@ -98,11 +98,17 @@ import {
`,
],
host: {
'[attr.role]': '"complementary"',
'[attr.aria-modal]': 'true',
'[attr.pfocustrap]': 'true',
'[attr.data-pc-name]': "'drawer'",
'[attr.data-pc-section]': "'root'",
'[attr.aria-hidden]': '!visible',
'[style.display]': 'visible ? "block" : "none"',
'[style.--sheet-transition]': 'transitionOptions',
},
})
export class SharedLightBottomsheetComponent implements OnInit, OnDestroy {
export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnChanges {
@Input() header = '';
@Input() visible = false;
@Output() visibleChange = new EventEmitter<boolean>();
@@ -116,6 +122,7 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy {
@Output() onHide = new EventEmitter<any>();
private originalParent: Node | null = null;
private readonly defaultDrawerZIndex = 1102;
constructor(
private readonly elementRef: ElementRef<HTMLElement>,
@@ -127,9 +134,20 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy {
const host = this.elementRef.nativeElement;
this.originalParent = host.parentNode;
this.renderer.appendChild(this.document.body, host);
this.syncZIndex();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['visible'] && this.visible) {
this.syncZIndex();
}
}
ngOnDestroy() {
this.removeDrawer();
}
private removeDrawer() {
const host = this.elementRef.nativeElement;
if (this.originalParent) {
this.renderer.appendChild(this.originalParent, host);
@@ -139,6 +157,9 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy {
onVisibilityChange(nextValue: boolean) {
this.visible = nextValue;
this.visibleChange.emit(nextValue);
if (nextValue) {
this.syncZIndex();
}
if (!nextValue) {
this.onHide.emit();
}
@@ -149,4 +170,19 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy {
this.onVisibilityChange(false);
}
}
private syncZIndex() {
const host = this.elementRef.nativeElement;
const siblingDrawers = Array.from(
this.document.body.querySelectorAll('.p-drawer'),
) as HTMLElement[];
const drawerZIndexes = siblingDrawers
.map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10))
.filter((zIndex) => Number.isFinite(zIndex) && zIndex > 0);
const maxDrawerZIndex = drawerZIndexes.length
? Math.max(...drawerZIndexes)
: this.defaultDrawerZIndex;
this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`);
}
}
@@ -7,6 +7,7 @@
[formControl]="control"
[showClear]="showClear"
[filter]="true"
[overlayOptions]="overlayOptions()"
appendTo="body"
/>
</uikit-field>
+22 -1
View File
@@ -1,5 +1,6 @@
import { UikitFieldComponent } from '@/uikit';
import { Component, computed, Input } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Component, Inject, computed, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { Observable } from 'rxjs';
@@ -16,6 +17,10 @@ export class EnumSelectComponent extends AbstractSelectComponent<ISelectItem, IS
@Input() type!: TLocalEnum;
@Input() customLabel?: string;
constructor(@Inject(DOCUMENT) private readonly document: Document) {
super();
}
label = computed(() => this.customLabel ?? LOCAL_ENUMS[this.type].label);
override getDataService() {
@@ -27,4 +32,20 @@ export class EnumSelectComponent extends AbstractSelectComponent<ISelectItem, IS
ngOnInit() {
this.getData();
}
overlayOptions() {
return {
autoZIndex: true,
baseZIndex: this.overlayBaseZIndex(),
};
}
overlayBaseZIndex() {
const drawers = Array.from(this.document.body.querySelectorAll('.p-drawer,[data-pc-name="drawer"]')) as HTMLElement[];
const zIndexes = drawers
.map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10))
.filter((zIndex) => Number.isFinite(zIndex) && zIndex > 0);
return zIndexes.length ? Math.max(...zIndexes) + 2 : 1000;
}
}