feat: enhance invoice correction form with item editing and price info display

This commit is contained in:
2026-06-08 13:35:58 +03:30
parent 788f4023f3
commit 5fa07c7ee8
12 changed files with 393 additions and 202 deletions
@@ -1,13 +1,11 @@
<div class="p-4"> <shared-sale-invoice-single-view
<shared-sale-invoice-single-view [loading]="loading()"
[loading]="loading()" [invoice]="invoice()"
[invoice]="invoice()" [backRoute]="backRoute()"
[backRoute]="backRoute()" [sendToTspLoading]="sendToTspLoading()"
[sendToTspLoading]="sendToTspLoading()" [inquiryLoading]="inquiryLoading()"
[inquiryLoading]="inquiryLoading()" (onSendToTsp)="sendToTsp()"
(onSendToTsp)="sendToTsp()" (onInquiry)="inquiry()"
(onInquiry)="inquiry()" (onCorrection)="correction($event)"
(onCorrection)="correction($event)" (onReturnFromSale)="returnFromSale($event)"
(onReturnFromSale)="returnFromSale($event)" variant="pos" />
variant="pos" />
</div>
@@ -1,8 +1,15 @@
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject } from '@angular/core'; import { Component, Input, inject } from '@angular/core';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
type TOrderPriceInfo = {
totalBaseAmount: number;
discountAmount: number;
taxAmount: number;
totalAmount: number;
};
@Component({ @Component({
selector: 'pos-order-price-info-card', selector: 'pos-order-price-info-card',
templateUrl: './price-info-card.component.html', templateUrl: './price-info-card.component.html',
@@ -10,6 +17,12 @@ import { PosLandingStore } from '../../store/main.store';
}) })
export class POSOrderPriceInfoCardComponent { export class POSOrderPriceInfoCardComponent {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
@Input() info?: TOrderPriceInfo;
priceInfo = computed(() => this.store.orderPricingInfo()); priceInfo(): TOrderPriceInfo {
if (this.info) {
return this.info;
}
return this.store.orderPricingInfo();
}
} }
@@ -1,25 +1,27 @@
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div> @if (rendered) {
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div>
<section class="light-bottomsheet-panel" [ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }"> <section class="light-bottomsheet-panel" [ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }">
<div class="light-bottomsheet-content"> <div class="light-bottomsheet-content">
@if (header || closable) { @if (header || closable) {
<header class="light-bottomsheet-header border-surface-border border-b"> <header class="light-bottomsheet-header border-surface-border border-b">
<span class="text-xl font-bold">{{ header }}</span> <span class="text-xl font-bold">{{ header }}</span>
@if (closable) { @if (closable) {
<p-button <p-button
type="button" type="button"
icon="pi pi-times" icon="pi pi-times"
text text
size="small" size="small"
class="light-bottomsheet-close" class="light-bottomsheet-close"
(click)="onVisibilityChange(false)" (click)="onVisibilityChange(false)"
aria-label="Close"> aria-label="Close">
</p-button> </p-button>
} }
</header> </header>
} }
<div class="light-bottomsheet-body p-4"> <div class="light-bottomsheet-body p-4">
<ng-content></ng-content> <ng-content></ng-content>
</div>
</div> </div>
</div> </section>
</section> }
@@ -90,8 +90,8 @@ import { Button } from 'primeng/button';
'[attr.pfocustrap]': 'true', '[attr.pfocustrap]': 'true',
'[attr.data-pc-name]': "'drawer'", '[attr.data-pc-name]': "'drawer'",
'[attr.data-pc-section]': "'root'", '[attr.data-pc-section]': "'root'",
'[attr.aria-hidden]': '!visible', '[attr.aria-hidden]': '!rendered',
'[style.display]': 'visible ? "block" : "none"', '[style.display]': 'rendered ? "block" : "none"',
'[style.--sheet-transition]': 'transitionOptions', '[style.--sheet-transition]': 'transitionOptions',
}, },
}) })
@@ -105,8 +105,11 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
@Input() style: Record<string, string> | undefined; @Input() style: Record<string, string> | undefined;
@Input() mobileDrawerHeight = '90vh'; @Input() mobileDrawerHeight = '90vh';
@Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)'; @Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)';
@Input() closeUnmountDelay = 150;
@Output() onHide = new EventEmitter<any>(); @Output() onHide = new EventEmitter<any>();
rendered = false;
private closeTimer: ReturnType<typeof setTimeout> | null = null;
private originalParent: Node | null = null; private originalParent: Node | null = null;
private readonly defaultDrawerZIndex = 1102; private readonly defaultDrawerZIndex = 1102;
@@ -123,21 +126,27 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
const host = this.elementRef.nativeElement; const host = this.elementRef.nativeElement;
this.originalParent = host.parentNode; this.originalParent = host.parentNode;
this.renderer.appendChild(this.document.body, host); this.renderer.appendChild(this.document.body, host);
this.rendered = this.visible;
this.syncZIndex(); this.syncZIndex();
this.toggleBodyScrollLock(this.visible); this.toggleBodyScrollLock(this.visible);
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['visible'] && this.visible) { if (changes['visible'] && this.visible) {
this.clearCloseTimer();
this.rendered = true;
this.syncZIndex(); this.syncZIndex();
} }
if (changes['visible']) { if (changes['visible']) {
this.toggleBodyScrollLock(this.visible); this.toggleBodyScrollLock(this.visible);
if (!this.visible) {
this.scheduleUnmount();
}
} }
} }
ngOnDestroy() { ngOnDestroy() {
this.clearCloseTimer();
this.toggleBodyScrollLock(false); this.toggleBodyScrollLock(false);
this.removeDrawer(); this.removeDrawer();
} }
@@ -154,9 +163,12 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
this.visibleChange.emit(nextValue); this.visibleChange.emit(nextValue);
this.toggleBodyScrollLock(nextValue); this.toggleBodyScrollLock(nextValue);
if (nextValue) { if (nextValue) {
this.clearCloseTimer();
this.rendered = true;
this.syncZIndex(); this.syncZIndex();
} }
if (!nextValue) { if (!nextValue) {
this.scheduleUnmount();
this.onHide.emit(); this.onHide.emit();
} }
} }
@@ -203,4 +215,19 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
: this.defaultDrawerZIndex; : this.defaultDrawerZIndex;
this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`); this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`);
} }
private scheduleUnmount() {
this.clearCloseTimer();
this.closeTimer = setTimeout(() => {
if (!this.visible) {
this.rendered = false;
}
}, this.closeUnmountDelay);
}
private clearCloseTimer() {
if (!this.closeTimer) return;
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
} }
@@ -3,7 +3,7 @@
<button <button
pButton pButton
[label]="submitLabel" [label]="submitLabel"
type="submit" [type]="submitBtnType"
class="min-w-28" class="min-w-28"
[disabled]="disabled" [disabled]="disabled"
[loading]="loading" [loading]="loading"
@@ -11,6 +11,7 @@ export class FormFooterActionsComponent {
@Input() cancelLabel = 'انصراف'; @Input() cancelLabel = 'انصراف';
@Input() loading = false; @Input() loading = false;
@Input() disabled = false; @Input() disabled = false;
@Input() submitBtnType: 'button' | 'submit' = 'submit';
@Output() onSubmit = new EventEmitter<void>(); @Output() onSubmit = new EventEmitter<void>();
@Output() onCancel = new EventEmitter<void>(); @Output() onCancel = new EventEmitter<void>();
@@ -1,4 +1,4 @@
<div class="flex items-center p-4"> <div class="bg-surface-card flex items-center p-4">
<div class="grow"> <div class="grow">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if (backRoute) { @if (backRoute) {
@@ -1,28 +1,78 @@
<form [formGroup]="form" (ngSubmit)="submit()"> <form [formGroup]="form" (ngSubmit)="submit()">
<field-invoice-date [control]="form.controls.invoice_date" /> <field-invoice-date [control]="form.controls.invoice_date" />
@for (item of initialValues || []; track item.id) { <div class="mt-4 flex flex-col gap-3">
<div class="py-3"> @for (item of initialValues || []; track item.id) {
@if ($index) { <div class="border-surface-border rounded-xl border p-2 shadow-sm">
<hr class="h-1 w-full" /> <div class="flex items-center gap-3">
} <img
<p-divider align="right"> [src]="item.good_snapshot.image_url || ''"
{{ $index + 1 }}- <b>{{ item.good_snapshot.name }}</b> loading="lazy"
</p-divider> decoding="async"
@if (isGold(item)) { class="bg-surface-100 border-surface-border h-20 w-20 shrink-0 rounded-md border object-cover" />
<shared-gold-payload-form <div class="flex grow flex-col gap-2 overflow-hidden py-1">
[initialValues]="$any(mappedInitialItems[$index])" <div class="flex items-center justify-between gap-2">
[vatPercentage]="vatFromItem(item)" <span class="overflow-hidden text-base font-bold text-nowrap text-ellipsis">
[isCorrection]="true" /> {{ item.good_snapshot.name }}
} @else { </span>
<shared-standard-payload-form <button
[initialValues]="$any(mappedInitialItems[$index])" pButton
[measureUnit]="item.good_snapshot.measure_unit" type="button"
[vatPercentage]="vatFromItem(item)" icon="pi pi-pencil"
[isCorrection]="true" /> size="small"
} outlined
</div> (click)="openItemEditor($index)"></button>
} </div>
@let preview = getItemPreview($index);
<span class="text-sm">{{ item.quantity }} {{ item.measure_unit_text }}</span>
<div class="flex flex-col">
@if (!preview.discount_amount) {
<span [appPriceMask]="preview.base_total_amount"></span>
} @else {
<div class="flex flex-col items-end">
<span class="text-muted-color text-xs line-through" [appPriceMask]="preview.base_total_amount"></span>
<span [appPriceMask]="preview.base_total_amount - preview.discount_amount"></span>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
<pos-order-price-info-card [info]="getTotalPriceInfo()" />
<app-form-footer-actions /> <app-form-footer-actions />
</form> </form>
<shared-light-bottomsheet
[(visible)]="showItemEditSheet"
[header]="`ویرایش ${activeItem()?.good_snapshot?.name || ''}`">
@if (activeItem()) {
<div class="pt-4">
@if (isGold(activeItem()!)) {
<shared-gold-payload-form
#goldEditor
[initialValues]="$any(activeInitialValue())"
[vatPercentage]="vatFromItem(activeItem()!)"
[isCorrection]="true" />
<div class="mt-6 flex justify-end gap-2">
<button pButton type="button" severity="secondary" (click)="backToList()">انصراف</button>
<button pButton type="button" (click)="saveActiveItemDraft(goldEditor)">ذخیره تغییرات</button>
</div>
} @else {
<shared-standard-payload-form
#standardEditor
[initialValues]="$any(activeInitialValue())"
[measureUnit]="activeItem()!.good_snapshot.measure_unit"
[vatPercentage]="vatFromItem(activeItem()!)"
[isCorrection]="true" />
<div class="mt-6 flex justify-end gap-2">
<button pButton type="button" severity="secondary" (click)="backToList()">انصراف</button>
<button pButton type="button" (click)="saveActiveItemDraft(undefined, standardEditor)">ذخیره تغییرات</button>
</div>
}
</div>
}
</shared-light-bottomsheet>
@@ -1,16 +1,19 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { POSOrderPriceInfoCardComponent } from '@/domains/pos/modules/shop/components/order/price-info-card.component';
import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { IPosOrderItem } from '@/domains/pos/modules/shop/models';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { import {
SharedGoldPayloadFormComponent, SharedGoldPayloadFormComponent,
SharedStandardPayloadFormComponent, SharedStandardPayloadFormComponent,
} from '@/shared/components'; } from '@/shared/components';
import { PriceMaskDirective } from '@/shared/directives';
import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType'; import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType';
import { IGoldPayload, IStandardPayload } from '@/shared/models'; import { IGoldPayload, IStandardPayload } from '@/shared/models';
import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils'; import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils';
import { Component, Input, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { Component, Input, signal, SimpleChanges } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { Divider } from 'primeng/divider'; import { ButtonDirective } from 'primeng/button';
import { SharedLightBottomsheetComponent } from '../../dialog';
import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component'; import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component';
import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component';
import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models'; import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models';
@@ -25,7 +28,10 @@ import { IInvoiceItem } from '../sale-invoice-full-response.model';
FormFooterActionsComponent, FormFooterActionsComponent,
SharedGoldPayloadFormComponent, SharedGoldPayloadFormComponent,
SharedStandardPayloadFormComponent, SharedStandardPayloadFormComponent,
Divider, ButtonDirective,
SharedLightBottomsheetComponent,
POSOrderPriceInfoCardComponent,
PriceMaskDirective,
], ],
}) })
export class SharedCorrectionFormComponent extends AbstractForm< export class SharedCorrectionFormComponent extends AbstractForm<
@@ -34,11 +40,7 @@ export class SharedCorrectionFormComponent extends AbstractForm<
IInvoiceItem[] IInvoiceItem[]
> { > {
@Input({ required: true }) invoiceDate!: string; @Input({ required: true }) invoiceDate!: string;
@Input() visible = false;
@ViewChildren(SharedGoldPayloadFormComponent)
goldForms!: QueryList<SharedGoldPayloadFormComponent>;
@ViewChildren(SharedStandardPayloadFormComponent)
standardForms!: QueryList<SharedStandardPayloadFormComponent>;
form = this.fb.group({ form = this.fb.group({
invoice_date: [this.invoiceDate], invoice_date: [this.invoiceDate],
@@ -46,6 +48,8 @@ export class SharedCorrectionFormComponent extends AbstractForm<
itemDrafts: Record<string, TCorrectionItemPayload> = {}; itemDrafts: Record<string, TCorrectionItemPayload> = {};
mappedInitialItems: TCorrectionItemPayload[] = []; mappedInitialItems: TCorrectionItemPayload[] = [];
showItemEditSheet = signal(false);
activeItemIndex = signal<number | null>(null);
initForm() { initForm() {
this.form.controls.invoice_date.setValue( this.form.controls.invoice_date.setValue(
@@ -53,6 +57,8 @@ export class SharedCorrectionFormComponent extends AbstractForm<
); );
this.itemDrafts = {}; this.itemDrafts = {};
this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item)); this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item));
this.showItemEditSheet.set(false);
this.activeItemIndex.set(null);
} }
override ngOnInit(): void { override ngOnInit(): void {
@@ -60,6 +66,11 @@ export class SharedCorrectionFormComponent extends AbstractForm<
} }
override ngOnChanges(changes: SimpleChanges) { override ngOnChanges(changes: SimpleChanges) {
if (changes['visible'] && !this.visible) {
this.initForm();
return;
}
if (changes['initialValues'] || changes['invoiceDate']) { if (changes['initialValues'] || changes['invoiceDate']) {
this.initForm(); this.initForm();
} }
@@ -115,23 +126,88 @@ export class SharedCorrectionFormComponent extends AbstractForm<
return this.mapToStandardInitialItem(item) as TCorrectionItemPayload; return this.mapToStandardInitialItem(item) as TCorrectionItemPayload;
} }
override submitForm() { openItemEditor(index: number) {
const goldQueue = [...(this.goldForms?.toArray() || [])]; this.activeItemIndex.set(index);
const standardQueue = [...(this.standardForms?.toArray() || [])]; this.showItemEditSheet.set(true);
const items = }
this.initialValues?.map((item, index) => {
const current = this.isGold(item)
? goldQueue.shift()?.getCorrectionValue()
: standardQueue.shift()?.getCorrectionValue();
const merged = (current || this.mappedInitialItems[index]) as TCorrectionItemPayload;
return {
...merged,
id: item.id,
pricing_model: item.good_snapshot?.pricing_model || '',
} as TCorrectionItemPayload;
}) || [];
console.log('items', items); backToList() {
this.showItemEditSheet.set(false);
this.activeItemIndex.set(null);
}
activeItem() {
const index = this.activeItemIndex();
if (index === null) return null;
return this.initialValues?.[index] || null;
}
activeInitialValue() {
const item = this.activeItem();
const index = this.activeItemIndex();
if (!item || index === null) return null;
return this.itemDrafts[item.id] || this.mappedInitialItems[index] || null;
}
getItemPreview(index: number) {
const item = this.initialValues?.[index];
return this.itemDrafts[item!.id] || this.mappedInitialItems[index] || null;
}
getTotalPriceInfo() {
const previews = (this.initialValues || [])
.map((_, index) => this.getItemPreview(index))
.filter(Boolean);
console.log('previews', previews);
return previews.reduce(
(acc, item: any) => {
acc.totalBaseAmount += Number(item.base_total_amount || 0);
acc.discountAmount += Number(item.discount_amount || 0);
acc.taxAmount += Number(item.tax_amount || 0);
acc.totalAmount += Number(item.total_amount || 0);
return acc;
},
{
totalBaseAmount: 0,
discountAmount: 0,
taxAmount: 0,
totalAmount: 0,
}
);
}
saveActiveItemDraft(
goldForm?: SharedGoldPayloadFormComponent,
standardForm?: SharedStandardPayloadFormComponent
) {
const item = this.activeItem();
const index = this.activeItemIndex();
if (!item || index === null) return;
const current = this.isGold(item)
? goldForm?.getCorrectionValue()
: standardForm?.getCorrectionValue();
if (!current) {
this.toastService.warn({ text: 'اطلاعات آیتم برای ذخیره در دسترس نیست.' });
return;
}
this.itemDrafts[item.id] = {
...(current as TCorrectionItemPayload),
id: item.id,
pricing_model: item.good_snapshot?.pricing_model || '',
};
this.backToList();
}
override submitForm() {
const items =
this.initialValues?.map(
(item, index) => this.itemDrafts[item.id] || this.mappedInitialItems[index]
) || [];
const hasChanges = (this.initialValues || []).some((item, index) => { const hasChanges = (this.initialValues || []).some((item, index) => {
const source = this.mappedInitialItems[index]; const source = this.mappedInitialItems[index];
@@ -3,20 +3,19 @@
} @else if (!invoice) { } @else if (!invoice) {
<uikit-empty-state> </uikit-empty-state> <uikit-empty-state> </uikit-empty-state>
} @else { } @else {
<shared-light-bottomsheet [visible]="actionLoading" [closable]="false" header="لطفا صبر کنید"> <p-dialog [visible]="actionLoading" [closable]="false" header="لطفا صبر کنید">
<div class="flex h-[30svh] flex-col items-center justify-center gap-6"> <div class="flex h-[30svh] flex-col items-center justify-center gap-6">
<p-progressSpinner /> <p-progressSpinner />
<span class="text-lg font-bold">در حال ارسال درخواست شما...</span> <span class="text-lg font-bold">در حال ارسال درخواست شما...</span>
</div> </div>
</shared-light-bottomsheet> </p-dialog>
<div class="flex flex-col gap-6"> <app-inner-pages-header pageTitle="اطلاعات صورت‌حساب‌" [backRoute]="backRoute">
<app-card-data cardTitle="اطلاعات صورت‌حساب‌" [editable]="false" [backRoute]="backRoute"> <ng-template #actions>
<ng-template #moreActions> <p-button (click)="printInvoice()" icon="pi pi-print" outlined size="small" label="چاپ" />
<div class=""> </ng-template>
<p-menu #menu [model]="moreActionMenuItems()" [popup]="true" appendTo="body" /> </app-inner-pages-header>
<p-button (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" outlined size="small" /> <div class="p-4">
</div> <p-card>
</ng-template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="listKeyValue"> <div class="listKeyValue">
<app-key-value label="شماره صورت‌حساب" [value]="invoice.invoice_number" /> <app-key-value label="شماره صورت‌حساب" [value]="invoice.invoice_number" />
@@ -93,24 +92,40 @@
<p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p> <p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p>
} }
} }
<p-divider align="center"> کالاهای خریداری شده </p-divider>
<app-page-data-list
[columns]="columns"
[items]="invoice.items"
[loading]="loading"
pageTitle=""
[showRefresh]="false">
<ng-template #totalAmount let-item>
@if (!item.discount_amount) {
<span [appPriceMask]="item.total_amount"></span>
}
</ng-template>
</app-page-data-list>
</div> </div>
</app-card-data> </p-card>
<app-card-data cardTitle="کالاهای خریداری شده" [editable]="false">
<app-page-data-list
[columns]="columns"
[items]="invoice.items"
[loading]="loading"
pageTitle=""
[showRefresh]="false">
<ng-template #totalAmount let-item>
@if (!item.discount_amount) {
<span [appPriceMask]="item.total_amount"></span>
}
</ng-template>
</app-page-data-list>
</app-card-data>
</div> </div>
@if (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) {
<button
pButton
[label]="action.label || ''"
[icon]="action.icon || ''"
size="small"
[severity]="$any(action).severity || 'secondary'"
[outlined]="$any(action).severity === 'secondary'"
(click)="action.command()"></button>
}
</div>
</div>
}
<shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش"> <shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش">
<shared-return-form <shared-return-form
[initialValues]="invoice.items" [initialValues]="invoice.items"
@@ -119,6 +134,7 @@
</shared-light-bottomsheet> </shared-light-bottomsheet>
<shared-light-bottomsheet [(visible)]="showCorrectionForm" header="اصلاحی"> <shared-light-bottomsheet [(visible)]="showCorrectionForm" header="اصلاحی">
<shared-correction-form <shared-correction-form
[visible]="showCorrectionForm()"
[initialValues]="invoice.items" [initialValues]="invoice.items"
[invoiceDate]="invoice.invoice_date" [invoiceDate]="invoice.invoice_date"
(onSubmit)="correctionSubmit($event)" /> (onSubmit)="correctionSubmit($event)" />
@@ -11,11 +11,7 @@ import {
TspProviderResponseStatus, TspProviderResponseStatus,
} from '@/shared/catalog'; } from '@/shared/catalog';
import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import { import { KeyValueComponent, SharedLightBottomsheetComponent } from '@/shared/components';
AppCardComponent,
KeyValueComponent,
SharedLightBottomsheetComponent,
} from '@/shared/components';
import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { import {
@@ -38,25 +34,32 @@ import {
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { UrlTree } from '@angular/router'; import { UrlTree } from '@angular/router';
import { MenuItem } from 'primeng/api'; import { Button, ButtonDirective } from 'primeng/button';
import { Button } from 'primeng/button'; import { Card } from 'primeng/card';
import { Dialog } from 'primeng/dialog';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { Menu } from 'primeng/menu';
import { ProgressSpinner } from 'primeng/progressspinner'; import { ProgressSpinner } from 'primeng/progressspinner';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service';
import { InnerPagesHeaderComponent } from '../innerPagesHeader/inner-pages-header.component';
import { SharedCorrectionFormComponent } from './correctionForm'; import { SharedCorrectionFormComponent } from './correctionForm';
import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models'; import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models';
import { SharedReturnFormComponent } from './returnForm/form.component'; import { SharedReturnFormComponent } from './returnForm/form.component';
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
type TActionMenuItem = {
label: string;
icon: string;
command: () => void;
neededStatus?: TspProviderResponseStatus;
severity?: 'secondary' | 'info' | 'warn' | 'danger' | 'contrast';
};
@Component({ @Component({
selector: 'shared-sale-invoice-single-view', selector: 'shared-sale-invoice-single-view',
templateUrl: './sale-invoice-single-view.component.html', templateUrl: './sale-invoice-single-view.component.html',
imports: [ imports: [
AppCardComponent,
KeyValueComponent, KeyValueComponent,
Divider, Divider,
PageDataListComponent, PageDataListComponent,
@@ -66,13 +69,16 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
PriceMaskDirective, PriceMaskDirective,
CatalogTaxProviderStatusTagComponent, CatalogTaxProviderStatusTagComponent,
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
Menu,
Button, Button,
CatalogInvoiceSettlementTypeTagComponent, CatalogInvoiceSettlementTypeTagComponent,
SharedLightBottomsheetComponent, SharedLightBottomsheetComponent,
ProgressSpinner, ProgressSpinner,
SharedReturnFormComponent, SharedReturnFormComponent,
SharedCorrectionFormComponent, SharedCorrectionFormComponent,
Dialog,
ButtonDirective,
InnerPagesHeaderComponent,
Card,
], ],
}) })
export class SharedSaleInvoiceSingleViewComponent { export class SharedSaleInvoiceSingleViewComponent {
@@ -100,7 +106,7 @@ export class SharedSaleInvoiceSingleViewComponent {
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>; @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
moreActionMenuItems = signal<MenuItem[]>([]); moreActionMenuItems = signal<TActionMenuItem[]>([]);
showCorrectionForm = signal(false); showCorrectionForm = signal(false);
showReturnFromSaleForm = signal(false); showReturnFromSaleForm = signal(false);
@@ -115,26 +121,85 @@ export class SharedSaleInvoiceSingleViewComponent {
this.inquiry = this.inquiry.bind(this); this.inquiry = this.inquiry.bind(this);
} }
showErrors = () => {};
inquiry = () => {
this.onInquiry.emit();
};
send = () => {
this.onSendToTsp.emit();
};
sendCorrection = (event: ICorrectionRequest) => {
this.onCorrection.emit(event);
};
resend = () => {
this.onResendToTsp.emit();
};
sendReturnFromSale = (event: IPosReturnFromSaleRequest) => {
this.onReturnFromSale.emit(event);
};
showCorrection = () => {
this.showCorrectionForm.set(true);
};
showReturnFromSale = () => {
this.showReturnFromSaleForm.set(true);
};
private readonly actions: TActionMenuItem[] = [
{
label: 'ارسال مجدد صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.SEND_FAILURE,
severity: 'info',
command: this.resend,
},
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.NOT_SEND,
severity: 'info',
command: this.send,
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: TspProviderResponseStatus.FISCAL_QUEUED,
severity: 'info',
command: this.inquiry,
},
{
label: 'دلایل خطا',
icon: 'pi pi-question',
neededStatus: TspProviderResponseStatus.FAILURE,
severity: 'danger',
command: this.showErrors,
},
{
label: 'ابطال',
icon: 'pi pi-eraser',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'danger',
command: this.showRevokeConfirmation,
},
{
label: 'اصلاح',
icon: 'pi pi-file-edit',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'warn',
command: this.showCorrection,
},
{
label: 'برگشت از فروش',
icon: 'pi pi-cart-minus',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'warn',
command: this.showReturnFromSale,
},
];
get actionLoading() { get actionLoading() {
return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading; return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading;
} }
showErrors() {}
inquiry() {
this.onInquiry.emit();
}
send() {
this.onSendToTsp.emit();
}
sendCorrection(event: ICorrectionRequest) {
this.onCorrection.emit(event);
}
resend() {
this.onResendToTsp.emit();
}
sendReturnFromSale(event: IPosReturnFromSaleRequest) {
this.onReturnFromSale.emit(event);
}
async showRevokeConfirmation() { async showRevokeConfirmation() {
await this.confirmationService.ask({ await this.confirmationService.ask({
header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`,
@@ -156,72 +221,13 @@ export class SharedSaleInvoiceSingleViewComponent {
}, },
}); });
} }
showCorrection() {
this.showCorrectionForm.set(true);
}
showReturnFromSale() {
this.showReturnFromSaleForm.set(true);
}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['invoice'] && this.invoice) { if (changes['invoice'] && this.invoice) {
const invoiceStatus = this.invoice.status.value; const invoiceStatus = this.invoice.status.value;
const actions: MenuItem[] = [
{
label: 'ارسال مجدد صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.SEND_FAILURE,
command: this.resend,
},
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.NOT_SEND,
command: this.send,
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: TspProviderResponseStatus.FISCAL_QUEUED,
command: this.inquiry,
},
{
label: 'دلایل خطا',
icon: 'pi pi-question',
neededStatus: TspProviderResponseStatus.FAILURE,
command: this.showErrors,
},
{
label: 'ابطال',
icon: 'pi pi-eraser',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showRevokeConfirmation,
},
{
label: 'اصلاح',
icon: 'pi pi-file-edit',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showCorrection,
},
{
label: 'برگشت از فروش',
icon: 'pi pi-cart-minus',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showReturnFromSale,
},
{
label: 'چاپ',
icon: 'pi pi-print',
neededStatus: '',
command: this.printInvoice,
},
];
this.moreActionMenuItems.set( this.moreActionMenuItems.set(
actions.filter( this.actions.filter((action) => action.neededStatus === invoiceStatus)
// @ts-ignore
(action) => !action.neededStatus || action.neededStatus === invoiceStatus
)
); );
} }
} }
@@ -2,7 +2,9 @@
<form [formGroup]="form"> <form [formGroup]="form">
<app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" /> <app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" />
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" /> <app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" /> @if (!isCorrection) {
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" />
}
<app-amount-percentage-input <app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.wages_percentage" [percentageControl]="form.controls.payload.controls.wages_percentage"