feat: update invoice components and add correction payment dialog
Production CI / validate-and-build (push) Failing after 1m1s

- Updated return form instructions for clarity.
- Refined labels in sale invoice info card for consistency.
- Enhanced sale invoice view with a correction payment dialog for increased user interaction.
- Improved invoice print preparation utility for better data handling.
- Adjusted paginator component for first and last page navigation.
- Modified layout styles for better responsiveness and user experience.
- Updated environment configurations for different setups.
This commit is contained in:
2026-06-14 16:33:51 +03:30
parent 72954fb5d1
commit 5ee03cf761
82 changed files with 709 additions and 544 deletions
@@ -43,7 +43,7 @@
<pos-order-price-info-card [info]="totalPriceInfo" />
<app-form-footer-actions />
<app-form-footer-actions [loading]="submitLoading()" />
</form>
<shared-light-bottomsheet
@@ -39,6 +39,7 @@ export class SharedCorrectionFormComponent extends AbstractForm<
CorrectionInvoiceFormValue,
IInvoiceItem[]
> {
@Input() beforeSubmit?: (payload: CorrectionInvoiceFormValue) => Promise<boolean> | boolean;
@Input({ required: true }) invoiceDate!: string;
@Input() visible = false;
@@ -212,7 +213,7 @@ export class SharedCorrectionFormComponent extends AbstractForm<
this.backToList();
}
override submitForm() {
private buildSubmitPayload(): CorrectionInvoiceFormValue | null {
const items =
this.initialValues?.map(
(item, index) => this.itemDrafts[item.id] || this.mappedInitialItems[index]
@@ -226,12 +227,33 @@ export class SharedCorrectionFormComponent extends AbstractForm<
if (!hasChanges) {
this.toastService.warn({ text: 'هیچ تغییری در مقادیر ثبت نشده است.' });
return;
return null;
}
this.onSubmit.emit({
return {
invoice_date: this.form.controls.invoice_date.value || '',
items,
});
};
}
override async submit() {
this.form.markAllAsTouched();
if (!this.form.valid) return;
const payload = this.buildSubmitPayload();
if (!payload) return;
this.submitLoading.set(true);
try {
const canSubmit = (await this.beforeSubmit?.(payload)) ?? true;
if (!canSubmit) return;
this.onSubmit.emit(payload);
} finally {
this.submitLoading.set(false);
}
}
override submitForm() {
// Submit flow is handled in overridden async submit()
}
}
@@ -0,0 +1,177 @@
import { INativePrintRequest } from '@/core/services/native-bridge.service';
import { formatJalali, formatWithCurrency } from '@/utils';
export function prepareInvoicePrintPayload(
invoice: any,
mustPrintConfig: Record<string, any>,
posName?: string
): INativePrintRequest[] | null {
if (!invoice) return null;
const defaultPrintItems: INativePrintRequest[] = [
{
title: 'اطلاعات صورت‌حساب',
items: [
{
label: 'شماره',
value: String(invoice.invoice_number ?? invoice.code ?? '-'),
show: true,
},
{
label: 'شماره منحصر به فرد مالیاتی',
value: String(invoice.tax_id || '-'),
show: true,
},
{
label: 'نوع',
value: String(invoice.type?.translate || '-'),
show: mustPrintConfig['invoice_template'],
},
{
label: 'موضوع',
value: String(invoice.pos?.complex?.business_activity?.guild?.name || '-'),
show: mustPrintConfig['invoice_template'] ?? true,
},
{
label: 'تاریخ',
value: String(invoice.invoice_date ? formatJalali(invoice.invoice_date) : '-'),
show: true,
},
{
label: 'وضعیت صدور',
value: String(invoice.status?.translate || '-'),
show: mustPrintConfig['show_payment_info'] ?? true,
},
]
.filter((item: any) => item.show)
.map(({ label, value }) => ({ label, value })),
},
{
title: `اطلاعات فروشنده`,
items: [
{
label: 'فروشگاه',
value: String(invoice.pos?.complex?.business_activity?.name || '-'),
show: mustPrintConfig['business_name'],
},
{
label: 'شعبه',
value: String(invoice.pos?.complex?.name || '-'),
show: mustPrintConfig['complex_name'],
},
{
label: 'پایانه‌ فروش',
value: String(invoice.pos?.name || posName || '-'),
show: mustPrintConfig['pos_name'],
},
{
label: 'شماره اقتصادی',
value: String(invoice.pos?.complex?.business_activity?.economic_code || '-'),
show: mustPrintConfig['economic_code'],
},
]
.filter((item: any) => item.show)
.map(({ label, value }) => ({ label, value })),
},
{
title: `اطلاعات خریدار`,
items: [
{
label: 'نام خریدار',
value: String(
(invoice.customer
? invoice.customer.type === 'LEGAL'
? invoice.customer.legal?.company_name
: `${invoice.customer.individual?.first_name || ''} ${
invoice.customer.individual?.last_name || ''
}`.trim()
: invoice.unknown_customer?.name) || '-'
),
show: mustPrintConfig['customer_name'],
},
{
label: 'شماره اقتصادی',
value: String(
(invoice.customer
? invoice.customer.type === 'LEGAL'
? invoice.customer.legal?.economic_code
: invoice.customer.individual?.economic_code || '-'
: '-') || '-'
),
show: mustPrintConfig['customer_economic_code'],
},
]
.filter((item: any) => item.show)
.map(({ label, value }) => ({ label, value })),
},
].filter((item) => item.items.length);
if (mustPrintConfig['show_items'] && Array.isArray(invoice.items)) {
for (const item of invoice.items) {
defaultPrintItems.push({
title: 'جزییات صورت‌حساب',
items: [
{
label: 'شرح',
value: String(item.good_snapshot?.name || '-'),
show: mustPrintConfig['item_name'] ?? true,
},
{
label: 'شناسه کالا / خدمت',
value: String(item.sku_code || '-'),
show: mustPrintConfig['item_sku'] ?? true,
},
{
label: 'تعداد / مقدار',
value: `${item.quantity || 0} ${item.measure_unit_text || ''}`.trim(),
show: mustPrintConfig['item_quantity'] ?? true,
},
{
label: 'قیمت واحد',
value: formatWithCurrency(item.unit_price || 0),
show: mustPrintConfig['item_base_amount'] ?? true,
},
{
label: 'اجرت',
value: formatWithCurrency(item.payload?.wages || 0),
show:
(mustPrintConfig['item_wage'] ?? true) && item.good_snapshot?.pricing_model === 'GOLD',
},
{
label: 'سود',
value: formatWithCurrency(item.payload?.profit || 0),
show:
(mustPrintConfig['item_profit'] ?? true) &&
item.good_snapshot?.pricing_model === 'GOLD',
},
{
label: 'حق‌العمل',
value: formatWithCurrency(item.payload?.commission || 0),
show:
(mustPrintConfig['item_commission'] ?? true) &&
item.good_snapshot?.pricing_model === 'GOLD',
},
{
label: 'مالیات بر ارزش افزوده',
value: formatWithCurrency(item.tax_amount || 0),
show: mustPrintConfig['item_tax'] ?? true,
},
{
label: 'تخفیف',
value: formatWithCurrency(item.discount_amount || 0),
show: mustPrintConfig['item_discount'] ?? true,
},
{
label: 'مبلغ کل',
value: formatWithCurrency(item.total_amount || 0),
show: mustPrintConfig['item_total_amount'] ?? true,
},
]
.filter((part: any) => part.show)
.map(({ label, value }) => ({ label, value })),
});
}
}
return defaultPrintItems;
}
@@ -3,7 +3,7 @@
<ol class="list-disc ps-4">
<li>تعداد محصولات/خدمات خود را کم و یا حذف کنید</li>
<li>حداقل ۱ خدمت/سرویس بایستی باقی بماند</li>
<li>تخفیف‌ها، مالیات و قیمت نهایی تمام شده با توجه به مقادیر فاکتور اصلی به صورت سیستمی محاسبه خواهند شد.</li>
<li>تخفیف‌ها، مالیات و مبلغ کل تمام شده با توجه به مقادیر فاکتور اصلی به صورت سیستمی محاسبه خواهند شد.</li>
</ol>
</p-message>
@@ -4,11 +4,11 @@
<div class="listKeyValue">
<app-key-value label="شماره منحصر به فرد مالیاتی" [value]="invoice.tax_id" />
<app-key-value label="تاریخ صورت‌حساب‌" [value]="invoice.invoice_date" type="date" />
<app-key-value label="تاریخ ایجاد صورت‌حساب‌" [value]="invoice.created_at" type="dateTime" />
<app-key-value label="نوع صورت‌حساب">
<app-key-value label="تاریخ ایجاد" [value]="invoice.created_at" type="dateTime" />
<app-key-value label="نوع">
<catalog-invoice-type-tag [status]="invoice.type.value" />
</app-key-value>
<app-key-value label="وضعیت صدور به سامانه‌ی مودیان">
<app-key-value label="وضعیت صدور">
<catalog-tax-provider-status-tag [status]="invoice.status.value" [translate]="invoice.status.translate" />
</app-key-value>
<div class="col-span-full">
@@ -17,8 +17,8 @@
</div>
<p-divider align="center"> اطلاعات مالی</p-divider>
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
<app-key-value label="مبلغ تخفیف" [value]="invoice.discount_amount" type="price" />
<app-key-value label=بلغ مالیات" [value]="invoice.tax_amount" type="price" />
<app-key-value label="تخفیف" [value]="invoice.discount_amount" type="price" />
<app-key-value label="مالیات" [value]="invoice.tax_amount" type="price" />
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
</div>
@@ -87,17 +87,17 @@ export class SaleInvoiceSingleInfoCardComponent {
},
{
field: 'discount_amount',
header: 'مبلغ تخفیف',
header: 'تخفیف',
type: 'price',
},
{
field: 'tax_amount',
header: بلغ مالیات',
header: 'مالیات',
type: 'price',
},
{
field: 'total_amount',
header: 'مبلغ قابل پرداخت',
header: 'قابل پرداخت',
type: 'price',
},
];
@@ -7,13 +7,18 @@
<ng-template #actions>
<p-button (click)="showQrCode()" icon="pi pi-qrcode" outlined size="small" />
<p-button (click)="printInvoice()" icon="pi pi-print" outlined size="small" label="چاپ" />
@if (!isApplication) {
<p-button (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" label="عملیات بیشتر" outlined size="small">
</p-button>
<p-menu #menu [model]="moreActionMenuItems()" [popup]="true"></p-menu>
}
</ng-template>
</app-inner-pages-header>
<div class="p-4">
<div class="py-4 max-md:p-4">
<shared-sale-invoice-single-info-card [loading]="loading" [invoice]="invoice" [variant]="variant" />
</div>
@if (moreActionMenuItems().length) {
@if (isApplication && moreActionMenuItems().length) {
<div class="border-surface-border bg-surface-card sticky bottom-0 z-10 mt-4 w-full rounded-t-2xl border-t p-3">
<div class="flex flex-wrap justify-center gap-2">
@for (action of moreActionMenuItems(); track action.label || $index) {
@@ -41,6 +46,7 @@
[visible]="showCorrectionForm()"
[initialValues]="invoice.items"
[invoiceDate]="invoice.invoice_date"
[beforeSubmit]="beforeCorrectionSubmitHandler.bind(this)"
(onSubmit)="correctionSubmit($event)" />
</shared-light-bottomsheet>
<shared-light-bottomsheet [(visible)]="showQrCodeDialog" header="کد QR">
@@ -53,4 +59,27 @@
</p-card>
</div>
</shared-light-bottomsheet>
<shared-light-bottomsheet [(visible)]="showCorrectionPaymentDialog" header="پرداخت اصلاحی">
<div class="flex flex-col gap-4">
<div class="text-sm">مبلغ اصلاحی افزایش یافته است. لطفاً مبلغ پرداختی را ثبت کنید.</div>
<div class="border-surface-border rounded-lg border p-3">
<div class="text-muted-color text-xs">حداقل مبلغ موردنیاز</div>
<div class="text-lg font-bold" [appPriceMask]="correctionRequiredPayment()"></div>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm font-medium">مبلغ پرداختی</label>
<input
type="number"
min="0"
class="border-surface-border w-full rounded-md border px-3 py-2"
[value]="correctionPaymentAmount()"
(input)="onCorrectionPaymentAmountChange($event)" />
</div>
<div class="flex justify-end gap-2">
<button pButton type="button" severity="secondary" label="انصراف" (click)="cancelCorrectionPayment()"></button>
<button pButton type="button" label="تایید پرداخت" (click)="confirmCorrectionPayment()"></button>
</div>
</div>
</shared-light-bottomsheet>
}
@@ -10,8 +10,8 @@ import { TspProviderResponseStatus } from '@/shared/catalog';
import { SharedLightBottomsheetComponent } from '@/shared/components';
import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model';
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit';
import { formatJalali, formatWithCurrency } from '@/utils';
import {
Component,
computed,
@@ -28,12 +28,15 @@ import { Router } from '@angular/router';
import { QRCodeComponent } from 'angularx-qrcode';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Menu } from 'primeng/menu';
import { TableModule } from 'primeng/table';
import { Observable } from 'rxjs';
import config from 'src/config';
import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service';
import { InnerPagesHeaderComponent } from '../innerPagesHeader/inner-pages-header.component';
import { SharedCorrectionFormComponent } from './correctionForm';
import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models';
import { prepareInvoicePrintPayload } from './prepare-print.util';
import { SharedReturnFormComponent } from './returnForm/form.component';
import {
SaleInvoiceSingleInfoCardComponent,
@@ -64,6 +67,8 @@ type TActionMenuItem = {
SaleInvoiceSingleInfoCardComponent,
QRCodeComponent,
Card,
PriceMaskDirective,
Menu,
],
})
export class SharedSaleInvoiceSingleViewComponent {
@@ -83,6 +88,7 @@ export class SharedSaleInvoiceSingleViewComponent {
@Input() sendToTspLoading: boolean = false;
@Input() resendToTspLoading: boolean = false;
@Input() inquiryLoading: boolean = false;
@Input() beforeCorrectionSubmit?: (data: ICorrectionRequest) => Promise<boolean> | boolean;
@Output() onRefresh = new EventEmitter<void>();
@Output() onSendToTsp = new EventEmitter<Observable<any>>();
@@ -97,6 +103,11 @@ export class SharedSaleInvoiceSingleViewComponent {
showCorrectionForm = signal(false);
showReturnFromSaleForm = signal(false);
showQrCodeDialog = signal(false);
showCorrectionPaymentDialog = signal(false);
correctionRequiredPayment = signal(0);
correctionPaymentAmount = signal(0);
readonly isApplication = config.isPosApplication;
private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean) => void> = null;
publicInvoiceRoute = computed(() => {
if (this.invoice) {
@@ -227,164 +238,7 @@ export class SharedSaleInvoiceSingleViewComponent {
preparePrint = async () => {
if (this.invoice) {
const mustPrintConfig = await this.service.getVisibleItems();
const defaultPrintItems = [
{
title: 'اطلاعات صورت‌حساب',
items: [
{
label: 'شماره',
value: this.invoice.invoice_number.toString(),
show: true,
},
{
label: 'شماره منحصر به فرد مالیاتی',
value: this.invoice.tax_id || '-',
show: true,
},
{
label: 'نوع',
value: this.invoice.type.translate,
show: mustPrintConfig.invoice_template,
},
{
label: 'موضوع',
value: this.invoice.pos.complex.business_activity.guild.name,
show: mustPrintConfig.invoice_template ?? true,
},
{
label: 'تاریخ',
value: formatJalali(this.invoice.invoice_date),
show: true,
},
{
label: 'وضعیت صدور',
value: this.invoice.status.translate,
show: mustPrintConfig.show_payment_info ?? true,
},
].filter((item) => item.show),
},
{
title: `اطلاعات فروشنده`,
items: [
{
label: 'فروشگاه',
value: this.invoice.pos.complex.business_activity.name,
show: mustPrintConfig.business_name,
},
{
label: 'شعبه',
value: this.invoice.pos.complex.name,
show: mustPrintConfig.complex_name,
},
{
label: 'پایانه‌ فروش',
value: this.invoice.pos.name,
show: mustPrintConfig.pos_name,
},
{
label: 'شماره اقتصادی',
value: this.invoice.pos.complex.business_activity.economic_code,
show: mustPrintConfig.economic_code,
},
].filter((item) => item.show),
},
{
title: `اطلاعات خریدار`,
items: [
{
label: 'نام خریدار',
value:
(this.invoice.customer
? this.invoice.customer.type === 'LEGAL'
? this.invoice.customer.legal?.company_name
: `${this.invoice.customer.individual?.first_name} ${this.invoice.customer.individual?.last_name}`
: this.invoice.unknown_customer?.name) || '-',
show: mustPrintConfig.customer_name,
},
{
label: 'شماره اقتصادی',
value:
(this.invoice.customer
? this.invoice.customer.type === 'LEGAL'
? this.invoice.customer.legal?.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_snapshot.name,
show: mustPrintConfig.item_name ?? true,
},
{
label: 'شناسه کالا / خدمت',
value: item.sku_code,
show: mustPrintConfig.item_sku ?? true,
},
{
label: 'تعداد / مقدار',
value: `${item.quantity} ${item.measure_unit_text}`,
show: mustPrintConfig.item_quantity ?? true,
},
{
label: 'قیمت واحد',
value: formatWithCurrency(item.unit_price),
show: mustPrintConfig.item_base_amount ?? true,
},
{
label: 'اجرت',
value: formatWithCurrency(item.payload.wages),
show:
(mustPrintConfig.item_wage ?? true) &&
item.good_snapshot.pricing_model === 'GOLD',
},
{
label: 'سود',
value: formatWithCurrency(item.payload.profit),
show:
(mustPrintConfig.item_profit ?? true) &&
item.good_snapshot.pricing_model === 'GOLD',
},
{
label: 'حق‌العمل',
value: formatWithCurrency(item.payload.commission),
show:
(mustPrintConfig.item_commission ?? true) &&
item.good_snapshot.pricing_model === 'GOLD',
},
{
label: 'مالیات بر ارزش افزوده',
value: formatWithCurrency(item.tax_amount),
show: mustPrintConfig.item_tax ?? true,
},
{
label: 'تخفیف',
value: formatWithCurrency(item.discount_amount),
show: mustPrintConfig.item_discount ?? true,
},
{
label: 'مبلغ کل',
value: formatWithCurrency(item.total_amount),
show: mustPrintConfig.item_total_amount ?? true,
},
],
});
}
}
console.log(defaultPrintItems);
return defaultPrintItems;
return prepareInvoicePrintPayload(this.invoice, mustPrintConfig, this.posName());
}
return null;
};
@@ -427,6 +281,53 @@ export class SharedSaleInvoiceSingleViewComponent {
this.sendCorrection(this.mapCorrectionRequest(event));
}
async beforeCorrectionSubmitHandler(event: CorrectionInvoiceFormValue): Promise<boolean> {
const mapped = this.mapCorrectionRequest(event);
const oldTotalAmount = Number(this.invoice?.total_amount || 0);
const newTotalAmount = Number(mapped.total_amount || 0);
if (newTotalAmount <= oldTotalAmount) {
return true;
}
const requiredAmount = newTotalAmount - oldTotalAmount;
this.correctionRequiredPayment.set(requiredAmount);
this.correctionPaymentAmount.set(requiredAmount);
this.showCorrectionPaymentDialog.set(true);
const allowByDialog = await new Promise<boolean>((resolve) => {
this.pendingCorrectionSubmitResolver = resolve;
});
if (!allowByDialog) {
return false;
}
return (await this.beforeCorrectionSubmit?.(mapped)) ?? true;
}
onCorrectionPaymentAmountChange(event: Event) {
const value = Number((event.target as HTMLInputElement)?.value || 0);
this.correctionPaymentAmount.set(Number.isFinite(value) ? value : 0);
}
confirmCorrectionPayment() {
if (this.correctionPaymentAmount() < this.correctionRequiredPayment()) {
this.toastService.warn({ text: 'مبلغ پرداختی باید حداقل برابر با افزایش مبلغ باشد.' });
return;
}
this.showCorrectionPaymentDialog.set(false);
this.pendingCorrectionSubmitResolver?.(true);
this.pendingCorrectionSubmitResolver = null;
}
cancelCorrectionPayment() {
this.showCorrectionPaymentDialog.set(false);
this.pendingCorrectionSubmitResolver?.(false);
this.pendingCorrectionSubmitResolver = null;
}
private mapReturnFromSaleRequest(event: ReturnFromSaleFormValue): IPosReturnFromSaleRequest {
const invoiceItems = this.invoice?.items || [];
const itemMap = new Map(invoiceItems.map((item) => [item.id, item]));