feat: refactor form components and improve invoice printing logic; enhance checkbox functionality and loading state management

This commit is contained in:
2026-05-17 10:59:15 +03:30
parent 73df354f9b
commit b2a7fa7f70
9 changed files with 302 additions and 116 deletions
@@ -1,11 +1,9 @@
import { brandingConfig } from '@/branding/branding.config'; import { brandingConfig } from '@/branding/branding.config';
import { Component, computed, signal } from '@angular/core'; import { Component, computed, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button';
@Component({ @Component({
selector: 'pos-about-page', selector: 'pos-about-page',
templateUrl: './root.component.html', templateUrl: './root.component.html',
imports: [ButtonDirective],
}) })
export class PosAboutPageComponent { export class PosAboutPageComponent {
// private readonly swUpdate = inject(SwUpdate); // private readonly swUpdate = inject(SwUpdate);
@@ -1,4 +1,5 @@
<form [formGroup]="form" (submit)="submit()"> @if (!loading()) {
<form [formGroup]="form" (submit)="submit()">
<app-checkbox [control]="form.controls.business_name" name="business_name" label="نمایش عنوان کسب‌و‌کار" /> <app-checkbox [control]="form.controls.business_name" name="business_name" label="نمایش عنوان کسب‌و‌کار" />
<app-checkbox [control]="form.controls.complex_name" name="complex_name" label="نمایش عنوان شعبه" /> <app-checkbox [control]="form.controls.complex_name" name="complex_name" label="نمایش عنوان شعبه" />
<app-checkbox [control]="form.controls.pos_name" name="pos_name" label="نمایش عنوان پایانه فروش" /> <app-checkbox [control]="form.controls.pos_name" name="pos_name" label="نمایش عنوان پایانه فروش" />
@@ -26,5 +27,6 @@
<app-checkbox [control]="form.controls.show_payment_info" name="show_payment_info" label="نمایش جزییات پرداخت" /> <app-checkbox [control]="form.controls.show_payment_info" name="show_payment_info" label="نمایش جزییات پرداخت" />
<app-checkbox [control]="form.controls.show_items" name="show_items" label="نمایش کالاهای خریداری شده" /> <app-checkbox [control]="form.controls.show_items" name="show_items" label="نمایش کالاهای خریداری شده" />
<app-form-footer-actions (onSubmit)="submit()" (onCancel)="close()" /> <app-form-footer-actions (onCancel)="close()" />
</form> </form>
}
@@ -1,7 +1,7 @@
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { AppCheckboxComponent } from '@/shared/components/checkbox/checkbox.component'; import { AppCheckboxComponent } from '@/shared/components/checkbox/checkbox.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, inject } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { IPosConfigPrintRequestPayload, IPosConfigPrintResponse } from './models'; import { IPosConfigPrintRequestPayload, IPosConfigPrintResponse } from './models';
import { PosConfigPrintService } from './services/main.service'; import { PosConfigPrintService } from './services/main.service';
@@ -17,6 +17,8 @@ export class PosConfigPrintFormComponent extends AbstractForm<
> { > {
private readonly service = inject(PosConfigPrintService); private readonly service = inject(PosConfigPrintService);
loading = signal(true);
initForm = () => { initForm = () => {
const form = this.fb.group({ const form = this.fb.group({
business_name: [true, []], business_name: [true, []],
@@ -40,8 +42,18 @@ export class PosConfigPrintFormComponent extends AbstractForm<
form = this.initForm(); form = this.initForm();
submitForm() { override async ngOnInit() {
this.loading.set(true);
const initialValues = await this.service.get();
console.log('initialValues', initialValues);
this.form.patchValue(initialValues);
this.loading.set(false);
}
override submitForm() {
const formValue = this.form.value as IPosConfigPrintRequestPayload; const formValue = this.form.value as IPosConfigPrintRequestPayload;
this.toastService.success({ text: 'تغییرات با موفقیت اعمال شد.' });
return this.service.submit(formValue); return this.service.submit(formValue);
} }
} }
@@ -21,7 +21,7 @@ export abstract class AbstractForm<
@Output() onClose = new EventEmitter<void>(); @Output() onClose = new EventEmitter<void>();
protected readonly fb = inject(FormBuilder); protected readonly fb = inject(FormBuilder);
private readonly toastService = inject(ToastService); protected readonly toastService = inject(ToastService);
constructor() {} constructor() {}
abstract form: ReturnType<FormBuilder['group']>; abstract form: ReturnType<FormBuilder['group']>;
@@ -1,6 +1,14 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<p-checkBox [formControl]="control" [name]="name" [inputId]="name" binary class="shrink-0"> </p-checkBox> <input
[id]="name"
type="checkbox"
[name]="name"
[disabled]="disabled"
[checked]="checked"
(change)="onChange($any($event.target).checked)"
class="h-5 w-5 shrink-0 cursor-pointer rounded border border-surface-300 accent-primary disabled:cursor-not-allowed disabled:opacity-60"
/>
<uikit-label [name]="name"> {{ label }} </uikit-label> <uikit-label [name]="name"> {{ label }} </uikit-label>
</div> </div>
@@ -2,12 +2,11 @@ import { Maybe } from '@/core';
import { UikitLabelComponent } from '@/uikit'; import { UikitLabelComponent } from '@/uikit';
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Checkbox } from 'primeng/checkbox';
@Component({ @Component({
selector: 'app-checkbox', selector: 'app-checkbox',
templateUrl: 'checkbox.component.html', templateUrl: 'checkbox.component.html',
imports: [Checkbox, UikitLabelComponent], imports: [UikitLabelComponent],
}) })
export class AppCheckboxComponent { export class AppCheckboxComponent {
@Input({ required: true }) control!: FormControl<Maybe<any>>; @Input({ required: true }) control!: FormControl<Maybe<any>>;
@@ -17,4 +16,14 @@ export class AppCheckboxComponent {
@Input() size?: 'small' | 'large'; @Input() size?: 'small' | 'large';
@Input() showErrors = true; @Input() showErrors = true;
@Input() hint?: string; @Input() hint?: string;
get checked() {
return this.control.value === 'true' || this.control.value === true;
}
onChange(checked: boolean) {
this.control.setValue(checked ? 'true' : 'false');
this.control.markAsDirty();
this.control.markAsTouched();
}
} }
@@ -22,7 +22,73 @@ export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {}
interface Item { interface Item {
id: string; id: string;
good: ISummary; good_id: string;
service_id: null;
quantity: string;
measure_unit_code: string;
measure_unit_text: string;
sku_code: string;
unit_price: string;
discount: string;
total_amount: string;
notes?: string;
payload: Payload;
good_snapshot: Goodsnapshot;
good: GoodSummary;
}
interface GoodSummary {
name: string;
barcode?: string;
image_url?: string;
category: Category;
}
interface Category {
id: string;
name: string;
description?: string;
image_url?: string;
is_default_guild_good: boolean;
guild_id: string;
created_at: string;
updated_at: string;
}
interface Goodsnapshot {
good: Good;
}
interface Good {
id: string;
sku: Sku;
name: string;
barcode?: string;
category: ISummary;
image_url: string;
local_sku?: string;
measure_unit: MeasureUnit;
pricing_model: string;
base_sale_price: string;
}
interface MeasureUnit {
id: string;
code: string;
name: string;
}
interface Sku {
id: string;
VAT: string;
code: string;
name: string;
}
interface Payload {
karat: string;
wages: number;
profit: number;
commission: number;
} }
interface Payment { interface Payment {
@@ -9,7 +9,7 @@
<button <button
pButton pButton
type="button" type="button"
label="چاپ کن" label="چاپ"
icon="pi pi-print" icon="pi pi-print"
outlined outlined
size="small" size="small"
@@ -1,6 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { NativeBridgeService } from '@/core/services'; import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service';
import { PosInfoStore } from '@/domains/pos/store'; import { PosInfoStore } from '@/domains/pos/store';
import { CatalogTaxProviderStatusTagComponent } from '@/shared/catalog'; import { CatalogTaxProviderStatusTagComponent } from '@/shared/catalog';
import { AppCardComponent, KeyValueComponent } from '@/shared/components'; import { AppCardComponent, KeyValueComponent } from '@/shared/components';
@@ -12,6 +13,7 @@ import {
} from '@/shared/components/pageDataList/page-data-list.component'; } from '@/shared/components/pageDataList/page-data-list.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit'; import { UikitEmptyStateComponent } from '@/uikit';
import { formatWithCurrency } from '@/utils';
import { import {
Component, Component,
computed, computed,
@@ -49,6 +51,8 @@ export class SharedSaleInvoiceSingleViewComponent {
private readonly nativeBridge = inject(NativeBridgeService); private readonly nativeBridge = inject(NativeBridgeService);
private readonly posInfoStore = inject(PosInfoStore); private readonly posInfoStore = inject(PosInfoStore);
private readonly toastService = inject(ToastService); private readonly toastService = inject(ToastService);
//TODO: below service Must be transform from pos to shared.
private readonly service = inject(PosConfigPrintService);
@Input({ required: true }) loading!: boolean; @Input({ required: true }) loading!: boolean;
@Input() invoice!: Maybe<ISaleInvoiceFullResponse>; @Input() invoice!: Maybe<ISaleInvoiceFullResponse>;
@@ -67,38 +71,45 @@ export class SharedSaleInvoiceSingleViewComponent {
return ''; return '';
}); });
printInvoice = () => { preparePrint = async () => {
try {
if (this.invoice) { if (this.invoice) {
this.nativeBridge.print([ const mustPrintConfig = await this.service.get();
const defaultPrintItems = [
{ {
title: 'اطلاعات صورت‌حساب', title: 'اطلاعات صورت‌حساب',
items: [ items: [
{ {
label: 'شماره منحصر به فرد مالیاتی', label: 'شماره منحصر به فرد مالیاتی',
value: 'this.invoice', value: 'this.invoice',
show: true,
}, },
{ {
label: 'شماره صورت‌حساب', label: 'شماره صورت‌حساب',
value: this.invoice.code, value: this.invoice.code,
show: true,
}, },
{ {
label: 'موضوع صورت‌حساب', label: 'موضوع صورت‌حساب',
value: 'this.invoice', value: 'this.invoice',
show: mustPrintConfig.invoice_template ?? true,
}, },
{ {
label: 'نوع صورت‌حساب', label: 'نوع صورت‌حساب',
value: 'this.invoice', value: 'this.invoice',
show: mustPrintConfig.invoice_template,
}, },
{ {
label: 'تاریخ و ساعت', label: 'تاریخ و ساعت',
value: this.invoice.invoice_date, value: this.invoice.invoice_date,
show: true,
}, },
{ {
label: 'وضعیت صورت‌حساب مالیاتی', label: 'وضعیت صورت‌حساب مالیاتی',
value: this.invoice.status.translate, value: this.invoice.status.translate,
show: mustPrintConfig.show_payment_info ?? true,
}, },
], ].filter((item) => item.show),
}, },
{ {
title: `اطلاعات فروشنده`, title: `اطلاعات فروشنده`,
@@ -106,20 +117,24 @@ export class SharedSaleInvoiceSingleViewComponent {
{ {
label: 'عنوان فروشگاه', label: 'عنوان فروشگاه',
value: this.invoice.pos.complex.business_activity.name, value: this.invoice.pos.complex.business_activity.name,
show: mustPrintConfig.business_name,
}, },
{ {
label: 'عنوان شعبه', label: 'عنوان شعبه',
value: this.invoice.pos.complex.name, value: this.invoice.pos.complex.name,
show: mustPrintConfig.complex_name,
}, },
{ {
label: 'عنوان پایانه‌ فروش', label: 'عنوان پایانه‌ فروش',
value: this.invoice.pos.name, value: this.invoice.pos.name,
show: mustPrintConfig.pos_name,
}, },
{ {
label: 'شماره اقتصادی', label: 'شماره اقتصادی',
value: 'this.invoice.pos.complex.business_activity.economic_code', value: 'this.invoice.pos.complex.business_activity.economic_code',
show: mustPrintConfig.economic_code,
}, },
], ].filter((item) => item.show),
}, },
{ {
title: `اطلاعات خریدار`, title: `اطلاعات خریدار`,
@@ -132,6 +147,7 @@ export class SharedSaleInvoiceSingleViewComponent {
? this.invoice.customer.legal?.company_name ? this.invoice.customer.legal?.company_name
: `${this.invoice.customer.individual?.first_name} ${this.invoice.customer.individual?.last_name}` : `${this.invoice.customer.individual?.first_name} ${this.invoice.customer.individual?.last_name}`
: this.invoice.unknown_customer?.name) || '-', : this.invoice.unknown_customer?.name) || '-',
show: mustPrintConfig.customer_name,
}, },
{ {
label: 'شماره اقتصادی', label: 'شماره اقتصادی',
@@ -141,10 +157,85 @@ export class SharedSaleInvoiceSingleViewComponent {
? this.invoice.customer.legal?.economic_code ? this.invoice.customer.legal?.economic_code
: this.invoice.customer.individual?.economic_code || '-' : this.invoice.customer.individual?.economic_code || '-'
: '-') || '-', : '-') || '-',
show: mustPrintConfig.customer_economic_code,
},
].filter((item) => item.show),
},
].filter((item) => item.items.length);
if (mustPrintConfig.show_items) {
for (let item of this.invoice.items) {
defaultPrintItems.push({
title: 'جزییات صورت‌حساب',
items: [
{
label: 'شرح',
value: item.good.name,
show: mustPrintConfig.items?.name,
},
{
label: 'شناسه کالا / خدمت',
value: item.sku_code,
show: mustPrintConfig.items?.sku,
},
{
label: 'تعداد / مقدار',
value: `${item.quantity} ${item.measure_unit_text}`,
show: mustPrintConfig.items?.quantity,
},
{
label: 'قیمت واحد',
value: formatWithCurrency(item.unit_price),
show: mustPrintConfig.items?.base_amount,
},
{
label: 'اجرت',
value: formatWithCurrency(item.payload.wages),
show:
mustPrintConfig.items?.wage && item.good_snapshot.good.pricing_model === 'GOLD',
},
{
label: 'سود',
value: formatWithCurrency(item.payload.wages),
show:
mustPrintConfig.items?.profit && item.good_snapshot.good.pricing_model === 'GOLD',
},
{
label: 'حق‌العمل',
value: formatWithCurrency(item.payload.commission),
show:
mustPrintConfig.items?.commission &&
item.good_snapshot.good.pricing_model === 'GOLD',
},
{
label: 'مالیات بر ارزش افزوده',
value: 'item.good.tax',
show: mustPrintConfig.items?.tax,
},
{
label: 'تخفیف',
value: formatWithCurrency(item.discount),
show: mustPrintConfig.items?.discount,
},
{
label: 'مبلغ کل',
value: formatWithCurrency(item.total_amount),
show: mustPrintConfig.items?.total_amount,
}, },
], ],
}, });
]); }
}
return defaultPrintItems;
}
return null;
};
printInvoice = async () => {
try {
const printItems = await this.preparePrint();
if (printItems) {
this.nativeBridge.print(printItems);
} }
} catch (error) { } catch (error) {
return this.toastService.error({ return this.toastService.error({