diff --git a/src/app/domains/pos/modules/saleInvoices/views/single.component.html b/src/app/domains/pos/modules/saleInvoices/views/single.component.html index 383cd66..b92f920 100644 --- a/src/app/domains/pos/modules/saleInvoices/views/single.component.html +++ b/src/app/domains/pos/modules/saleInvoices/views/single.component.html @@ -1,13 +1,11 @@ -
- -
+ diff --git a/src/app/domains/pos/modules/shop/components/order/price-info-card.component.ts b/src/app/domains/pos/modules/shop/components/order/price-info-card.component.ts index 817c831..5bab2e5 100644 --- a/src/app/domains/pos/modules/shop/components/order/price-info-card.component.ts +++ b/src/app/domains/pos/modules/shop/components/order/price-info-card.component.ts @@ -1,8 +1,15 @@ 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 { PosLandingStore } from '../../store/main.store'; +type TOrderPriceInfo = { + totalBaseAmount: number; + discountAmount: number; + taxAmount: number; + totalAmount: number; +}; + @Component({ selector: 'pos-order-price-info-card', templateUrl: './price-info-card.component.html', @@ -10,6 +17,12 @@ import { PosLandingStore } from '../../store/main.store'; }) export class POSOrderPriceInfoCardComponent { 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(); + } } diff --git a/src/app/shared/components/dialog/light-bottomsheet.component.html b/src/app/shared/components/dialog/light-bottomsheet.component.html index 6f013b9..b045bb5 100644 --- a/src/app/shared/components/dialog/light-bottomsheet.component.html +++ b/src/app/shared/components/dialog/light-bottomsheet.component.html @@ -1,25 +1,27 @@ -
+@if (rendered) { +
-
-
- @if (header || closable) { -
- {{ header }} - @if (closable) { - - - } -
- } -
- +
+
+ @if (header || closable) { +
+ {{ header }} + @if (closable) { + + + } +
+ } +
+ +
-
-
+ +} diff --git a/src/app/shared/components/dialog/light-bottomsheet.component.ts b/src/app/shared/components/dialog/light-bottomsheet.component.ts index 38e2396..0546d4e 100644 --- a/src/app/shared/components/dialog/light-bottomsheet.component.ts +++ b/src/app/shared/components/dialog/light-bottomsheet.component.ts @@ -90,8 +90,8 @@ import { Button } from 'primeng/button'; '[attr.pfocustrap]': 'true', '[attr.data-pc-name]': "'drawer'", '[attr.data-pc-section]': "'root'", - '[attr.aria-hidden]': '!visible', - '[style.display]': 'visible ? "block" : "none"', + '[attr.aria-hidden]': '!rendered', + '[style.display]': 'rendered ? "block" : "none"', '[style.--sheet-transition]': 'transitionOptions', }, }) @@ -105,8 +105,11 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha @Input() style: Record | undefined; @Input() mobileDrawerHeight = '90vh'; @Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)'; + @Input() closeUnmountDelay = 150; @Output() onHide = new EventEmitter(); + rendered = false; + private closeTimer: ReturnType | null = null; private originalParent: Node | null = null; private readonly defaultDrawerZIndex = 1102; @@ -123,21 +126,27 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha const host = this.elementRef.nativeElement; this.originalParent = host.parentNode; this.renderer.appendChild(this.document.body, host); + this.rendered = this.visible; this.syncZIndex(); this.toggleBodyScrollLock(this.visible); } ngOnChanges(changes: SimpleChanges) { if (changes['visible'] && this.visible) { + this.clearCloseTimer(); + this.rendered = true; this.syncZIndex(); } if (changes['visible']) { - this.toggleBodyScrollLock(this.visible); + if (!this.visible) { + this.scheduleUnmount(); + } } } ngOnDestroy() { + this.clearCloseTimer(); this.toggleBodyScrollLock(false); this.removeDrawer(); } @@ -154,9 +163,12 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha this.visibleChange.emit(nextValue); this.toggleBodyScrollLock(nextValue); if (nextValue) { + this.clearCloseTimer(); + this.rendered = true; this.syncZIndex(); } if (!nextValue) { + this.scheduleUnmount(); this.onHide.emit(); } } @@ -203,4 +215,19 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha : this.defaultDrawerZIndex; 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; + } } diff --git a/src/app/shared/components/formFooterActions/form-footer-actions.component.html b/src/app/shared/components/formFooterActions/form-footer-actions.component.html index 5f79c53..6eb241d 100644 --- a/src/app/shared/components/formFooterActions/form-footer-actions.component.html +++ b/src/app/shared/components/formFooterActions/form-footer-actions.component.html @@ -3,7 +3,7 @@ + + @let preview = getItemPreview($index); + {{ item.quantity }} {{ item.measure_unit_text }} +
+ @if (!preview.discount_amount) { + + } @else { +
+ + +
+ } +
+ + + + } + + + + + + @if (activeItem()) { +
+ @if (isGold(activeItem()!)) { + +
+ + +
+ } @else { + +
+ + +
+ } +
+ } +
diff --git a/src/app/shared/components/invoices/correctionForm/form.component.ts b/src/app/shared/components/invoices/correctionForm/form.component.ts index 5eb074b..fe70424 100644 --- a/src/app/shared/components/invoices/correctionForm/form.component.ts +++ b/src/app/shared/components/invoices/correctionForm/form.component.ts @@ -1,16 +1,19 @@ 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 { AbstractForm } from '@/shared/abstractClasses'; import { SharedGoldPayloadFormComponent, SharedStandardPayloadFormComponent, } from '@/shared/components'; +import { PriceMaskDirective } from '@/shared/directives'; import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType'; import { IGoldPayload, IStandardPayload } from '@/shared/models'; 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 { Divider } from 'primeng/divider'; +import { ButtonDirective } from 'primeng/button'; +import { SharedLightBottomsheetComponent } from '../../dialog'; import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component'; import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component'; import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models'; @@ -25,7 +28,10 @@ import { IInvoiceItem } from '../sale-invoice-full-response.model'; FormFooterActionsComponent, SharedGoldPayloadFormComponent, SharedStandardPayloadFormComponent, - Divider, + ButtonDirective, + SharedLightBottomsheetComponent, + POSOrderPriceInfoCardComponent, + PriceMaskDirective, ], }) export class SharedCorrectionFormComponent extends AbstractForm< @@ -34,11 +40,7 @@ export class SharedCorrectionFormComponent extends AbstractForm< IInvoiceItem[] > { @Input({ required: true }) invoiceDate!: string; - - @ViewChildren(SharedGoldPayloadFormComponent) - goldForms!: QueryList; - @ViewChildren(SharedStandardPayloadFormComponent) - standardForms!: QueryList; + @Input() visible = false; form = this.fb.group({ invoice_date: [this.invoiceDate], @@ -46,6 +48,8 @@ export class SharedCorrectionFormComponent extends AbstractForm< itemDrafts: Record = {}; mappedInitialItems: TCorrectionItemPayload[] = []; + showItemEditSheet = signal(false); + activeItemIndex = signal(null); initForm() { this.form.controls.invoice_date.setValue( @@ -53,6 +57,8 @@ export class SharedCorrectionFormComponent extends AbstractForm< ); this.itemDrafts = {}; this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item)); + this.showItemEditSheet.set(false); + this.activeItemIndex.set(null); } override ngOnInit(): void { @@ -60,6 +66,11 @@ export class SharedCorrectionFormComponent extends AbstractForm< } override ngOnChanges(changes: SimpleChanges) { + if (changes['visible'] && !this.visible) { + this.initForm(); + return; + } + if (changes['initialValues'] || changes['invoiceDate']) { this.initForm(); } @@ -115,23 +126,88 @@ export class SharedCorrectionFormComponent extends AbstractForm< return this.mapToStandardInitialItem(item) as TCorrectionItemPayload; } - override submitForm() { - const goldQueue = [...(this.goldForms?.toArray() || [])]; - const standardQueue = [...(this.standardForms?.toArray() || [])]; - 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; - }) || []; + openItemEditor(index: number) { + this.activeItemIndex.set(index); + this.showItemEditSheet.set(true); + } - 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 source = this.mappedInitialItems[index]; diff --git a/src/app/shared/components/invoices/sale-invoice-single-view.component.html b/src/app/shared/components/invoices/sale-invoice-single-view.component.html index b6dcc1f..c780c14 100644 --- a/src/app/shared/components/invoices/sale-invoice-single-view.component.html +++ b/src/app/shared/components/invoices/sale-invoice-single-view.component.html @@ -3,20 +3,19 @@ } @else if (!invoice) { } @else { - +
در حال ارسال درخواست شما...
-
-
- - -
- - -
-
+ + + + + + +
+
@@ -93,24 +92,40 @@

اطلاعات مشتری ثبت نشده است.

} } + کالاهای خریداری شده + + + @if (!item.discount_amount) { + + } + +
- - - - - @if (!item.discount_amount) { - - } - - - +
+ @if (moreActionMenuItems().length) { +
+
+ @for (action of moreActionMenuItems(); track action.label || $index) { + + } +
+
+ } + diff --git a/src/app/shared/components/invoices/sale-invoice-single-view.component.ts b/src/app/shared/components/invoices/sale-invoice-single-view.component.ts index 82f606c..a3c77a8 100644 --- a/src/app/shared/components/invoices/sale-invoice-single-view.component.ts +++ b/src/app/shared/components/invoices/sale-invoice-single-view.component.ts @@ -11,11 +11,7 @@ import { TspProviderResponseStatus, } from '@/shared/catalog'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; -import { - AppCardComponent, - KeyValueComponent, - SharedLightBottomsheetComponent, -} from '@/shared/components'; +import { KeyValueComponent, SharedLightBottomsheetComponent } from '@/shared/components'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { @@ -38,25 +34,32 @@ import { ViewChild, } from '@angular/core'; import { UrlTree } from '@angular/router'; -import { MenuItem } from 'primeng/api'; -import { Button } from 'primeng/button'; +import { Button, ButtonDirective } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Dialog } from 'primeng/dialog'; import { Divider } from 'primeng/divider'; -import { Menu } from 'primeng/menu'; import { ProgressSpinner } from 'primeng/progressspinner'; import { TableModule } from 'primeng/table'; import { Observable } from 'rxjs'; 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 { SharedReturnFormComponent } from './returnForm/form.component'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; +type TActionMenuItem = { + label: string; + icon: string; + command: () => void; + neededStatus?: TspProviderResponseStatus; + severity?: 'secondary' | 'info' | 'warn' | 'danger' | 'contrast'; +}; @Component({ selector: 'shared-sale-invoice-single-view', templateUrl: './sale-invoice-single-view.component.html', imports: [ - AppCardComponent, KeyValueComponent, Divider, PageDataListComponent, @@ -66,13 +69,16 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; PriceMaskDirective, CatalogTaxProviderStatusTagComponent, CatalogInvoiceTypeTagComponent, - Menu, Button, CatalogInvoiceSettlementTypeTagComponent, SharedLightBottomsheetComponent, ProgressSpinner, SharedReturnFormComponent, SharedCorrectionFormComponent, + Dialog, + ButtonDirective, + InnerPagesHeaderComponent, + Card, ], }) export class SharedSaleInvoiceSingleViewComponent { @@ -100,7 +106,7 @@ export class SharedSaleInvoiceSingleViewComponent { @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef; - moreActionMenuItems = signal([]); + moreActionMenuItems = signal([]); showCorrectionForm = signal(false); showReturnFromSaleForm = signal(false); @@ -115,26 +121,85 @@ export class SharedSaleInvoiceSingleViewComponent { 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() { 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() { await this.confirmationService.ask({ 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) { if (changes['invoice'] && this.invoice) { 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( - actions.filter( - // @ts-ignore - (action) => !action.neededStatus || action.neededStatus === invoiceStatus - ) + this.actions.filter((action) => action.neededStatus === invoiceStatus) ); } } diff --git a/src/app/shared/components/posPayment/gold-form.component.html b/src/app/shared/components/posPayment/gold-form.component.html index f8a3c26..f6f7f00 100644 --- a/src/app/shared/components/posPayment/gold-form.component.html +++ b/src/app/shared/components/posPayment/gold-form.component.html @@ -2,7 +2,9 @@
- + @if (!isCorrection) { + + }