From b4cd4c05f23a530ff27ded8d29358d6b5ef24a00 Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Thu, 11 Jun 2026 16:13:48 +0330 Subject: [PATCH] feat: enhance inner pages header with back button functionality and emit event on back click fix: update total price info handling in correction form and ensure accurate calculations refactor: streamline sale invoice single view component by extracting info card into a separate component style: adjust button sizes in payment forms for better UI consistency feat: implement season picker navigation controls to prevent out-of-bounds selection fix: improve price formatting utility to handle undefined values gracefully chore: update environment configuration for API base URL feat: add navigation service to manage routing history and back navigation --- src/app.component.ts | 6 +- .../core/services/native-bridge.service.ts | 2 + src/app/core/services/navigation.service.ts | 51 +++++ .../components/print/form.component.html | 11 +- .../components/print/form.component.ts | 9 +- .../configs/components/print/models/index.ts | 24 +- .../components/print/services/main.service.ts | 47 ++-- .../saleInvoices/views/single.component.html | 8 +- .../saleInvoices/views/single.component.ts | 16 +- .../dialog/light-bottomsheet.component.html | 4 +- .../dialog/light-bottomsheet.component.ts | 30 ++- .../inner-pages-header.component.html | 14 +- .../inner-pages-header.component.ts | 3 +- .../correctionForm/form.component.html | 2 +- .../invoices/correctionForm/form.component.ts | 19 +- .../sale-invoice-full-response.model.ts | 10 +- ...le-invoice-single-info-card.component.html | 93 ++++++++ ...sale-invoice-single-info-card.component.ts | 104 +++++++++ .../sale-invoice-single-view.component.html | 105 ++------- .../sale-invoice-single-view.component.ts | 210 +++++++----------- .../posPayment/gold-form.component.html | 2 +- .../posPayment/standard-form.component.html | 2 +- .../season-picker-dialog.component.html | 13 +- .../season-picker-dialog.component.ts | 2 + .../seasonPicker/season-picker.component.html | 16 +- .../seasonPicker/season-picker.component.ts | 27 ++- src/app/utils/price-mask.utils.ts | 2 +- src/environments/environment.tis.ts | 4 +- 28 files changed, 526 insertions(+), 310 deletions(-) create mode 100644 src/app/core/services/navigation.service.ts create mode 100644 src/app/shared/components/invoices/sale-invoice-single-info-card.component.html create mode 100644 src/app/shared/components/invoices/sale-invoice-single-info-card.component.ts diff --git a/src/app.component.ts b/src/app.component.ts index 2f3aa52..c0a6cdb 100644 --- a/src/app.component.ts +++ b/src/app.component.ts @@ -1,3 +1,4 @@ +import { NavigationService } from '@/core/services/navigation.service'; import { PwaInstallService } from '@/core/services/pwa-install.service'; import { ConfirmationDialogComponent } from '@/shared/components/confirmationDialog/confirmation-dialog.component'; import { Component, HostListener } from '@angular/core'; @@ -18,7 +19,10 @@ import { brandingConfig } from './app/branding/branding.config'; export class AppComponent { toastPosition: 'top-center' | 'bottom-right' = 'bottom-right'; - constructor(private readonly pwaInstallService: PwaInstallService) { + constructor( + private readonly pwaInstallService: PwaInstallService, + private readonly navigationService: NavigationService + ) { this.updateToastPosition(); if (brandingConfig.enableInstallPrompt) { this.pwaInstallService.init(); diff --git a/src/app/core/services/native-bridge.service.ts b/src/app/core/services/native-bridge.service.ts index 94cc0b4..8a20bf7 100644 --- a/src/app/core/services/native-bridge.service.ts +++ b/src/app/core/services/native-bridge.service.ts @@ -135,6 +135,8 @@ export class NativeBridgeService { this.toastService.info({ text: 'در حال چاپ ...' }); // @ts-ignore window.NativeBridge.print(JSON.stringify(payload)); + console.log('payload', payload); + return { success: true }; } catch (error) { this.toastService.warn({ text: 'متاسفانه ارتباط با چاپگر برقرار نیست.' }); diff --git a/src/app/core/services/navigation.service.ts b/src/app/core/services/navigation.service.ts new file mode 100644 index 0000000..f67ab9e --- /dev/null +++ b/src/app/core/services/navigation.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class NavigationService { + private readonly CURRENT_URL_KEY = 'currentUrl'; + private readonly PREVIOUS_URL_KEY = 'previousUrl'; + + currentUrl: string | null; + previousUrl: string | null; + + constructor(private router: Router) { + this.currentUrl = sessionStorage.getItem(this.CURRENT_URL_KEY); + this.previousUrl = sessionStorage.getItem(this.PREVIOUS_URL_KEY); + + this.router.events + .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) + .subscribe((event) => { + const newUrl = event.urlAfterRedirects; + + // Ignore duplicate events + if (newUrl === this.currentUrl) { + return; + } + + this.previousUrl = this.currentUrl; + this.currentUrl = newUrl; + + if (this.previousUrl) { + sessionStorage.setItem(this.PREVIOUS_URL_KEY, this.previousUrl); + } + + sessionStorage.setItem(this.CURRENT_URL_KEY, this.currentUrl); + }); + } + + canGoBack(): boolean { + return !!this.previousUrl; + } + + back(fallbackUrl = '/'): void { + if (this.canGoBack()) { + window.history.back(); + } else { + this.router.navigateByUrl(fallbackUrl); + } + } +} diff --git a/src/app/domains/pos/modules/configs/components/print/form.component.html b/src/app/domains/pos/modules/configs/components/print/form.component.html index 59b2474..fc1b1d9 100644 --- a/src/app/domains/pos/modules/configs/components/print/form.component.html +++ b/src/app/domains/pos/modules/configs/components/print/form.component.html @@ -11,22 +11,17 @@ + label="نمایش کد ملی خریدار" /> + label="نمایش کد پستی خریدار" /> + label="نمایش شناسه‌ملی / اقتصادی خریدار" /> - - } diff --git a/src/app/domains/pos/modules/configs/components/print/form.component.ts b/src/app/domains/pos/modules/configs/components/print/form.component.ts index cfecd87..4f57f20 100644 --- a/src/app/domains/pos/modules/configs/components/print/form.component.ts +++ b/src/app/domains/pos/modules/configs/components/print/form.component.ts @@ -1,6 +1,5 @@ import { AbstractForm } from '@/shared/abstractClasses'; import { AppCheckboxComponent } from '@/shared/components/checkbox/checkbox.component'; -import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { Component, inject, signal } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { IPosConfigPrintRequestPayload, IPosConfigPrintResponse } from './models'; @@ -9,7 +8,7 @@ import { PosConfigPrintService } from './services/main.service'; @Component({ selector: 'pos-config-print-form', templateUrl: 'form.component.html', - imports: [ReactiveFormsModule, AppCheckboxComponent, FormFooterActionsComponent], + imports: [ReactiveFormsModule, AppCheckboxComponent], }) export class PosConfigPrintFormComponent extends AbstractForm< IPosConfigPrintRequestPayload, @@ -37,6 +36,10 @@ export class PosConfigPrintFormComponent extends AbstractForm< show_items: [false], }); + form.valueChanges.subscribe(() => { + this.submitForm(); + }); + return form; }; @@ -52,7 +55,7 @@ export class PosConfigPrintFormComponent extends AbstractForm< override submitForm() { const formValue = this.form.value as IPosConfigPrintRequestPayload; - this.toastService.success({ text: 'تغییرات با موفقیت اعمال شد.' }); + // this.toastService.success({ text: 'تغییرات با موفقیت اعمال شد.' }); return this.service.submit(formValue); } } diff --git a/src/app/domains/pos/modules/configs/components/print/models/index.ts b/src/app/domains/pos/modules/configs/components/print/models/index.ts index 0d5edfd..03efa9d 100644 --- a/src/app/domains/pos/modules/configs/components/print/models/index.ts +++ b/src/app/domains/pos/modules/configs/components/print/models/index.ts @@ -15,17 +15,15 @@ export interface IPosConfigPrintRequestPayload { payment_type: boolean; show_payment_info: boolean; show_items: boolean; - items: { - name: boolean; - sku: boolean; - quantity: boolean; - base_amount: boolean; - tax: boolean; - discount: boolean; - karat: boolean; - profit: boolean; - commission: boolean; - wage: boolean; - total_amount: boolean; - }; + item_name: boolean; + item_sku: boolean; + item_quantity: boolean; + item_base_amount: boolean; + item_tax: boolean; + item_discount: boolean; + item_karat: boolean; + item_profit: boolean; + item_commission: boolean; + item_wage: boolean; + item_total_amount: boolean; } diff --git a/src/app/domains/pos/modules/configs/components/print/services/main.service.ts b/src/app/domains/pos/modules/configs/components/print/services/main.service.ts index f8f1e49..79713dd 100644 --- a/src/app/domains/pos/modules/configs/components/print/services/main.service.ts +++ b/src/app/domains/pos/modules/configs/components/print/services/main.service.ts @@ -7,7 +7,12 @@ export class PosConfigPrintService { get(): IPosConfigPrintResponse { const data = window.localStorage.getItem(LOCAL_STORAGE_KEYS.POS_CONFIG_PRINT); if (data) { - return JSON.parse(data) as IPosConfigPrintResponse; + const dataObject = JSON.parse(data) as Record; + return Object.keys(dataObject).reduce((prev, curr) => { + const key = curr as keyof IPosConfigPrintResponse; + prev[key] = dataObject[key] === 'true'; + return prev; + }, {} as IPosConfigPrintResponse); } const defaultData = { business_name: true, @@ -23,25 +28,37 @@ export class PosConfigPrintService { customer_economic_code: true, payment_type: true, show_payment_info: true, - show_items: false, - items: { - name: false, - sku: false, - quantity: false, - base_amount: false, - tax: false, - discount: false, - karat: false, - profit: false, - commission: false, - wage: false, - total_amount: false, - }, + show_items: true, + + item_name: false, + item_sku: false, + item_quantity: false, + item_base_amount: false, + item_tax: false, + item_discount: false, + item_karat: false, + item_profit: false, + item_commission: false, + item_wage: false, + item_total_amount: false, }; this.submit(defaultData); return defaultData; } + getVisibleItems() { + const items = this.get(); + return Object.keys(items).reduce((result, key) => { + const itemKey = key as keyof IPosConfigPrintRequestPayload; + + if (items[itemKey]) { + result[itemKey] = items[itemKey]; + } + + return result; + }, {} as Partial); + } + submit(data: IPosConfigPrintRequestPayload) { window.localStorage.setItem(LOCAL_STORAGE_KEYS.POS_CONFIG_PRINT, JSON.stringify(data)); } diff --git a/src/app/modules/saleInvoices/views/single.component.html b/src/app/modules/saleInvoices/views/single.component.html index eda4480..e843c62 100644 --- a/src/app/modules/saleInvoices/views/single.component.html +++ b/src/app/modules/saleInvoices/views/single.component.html @@ -1,3 +1,9 @@
- + +
+ صورت‌حساب شماره {{ invoice()?.invoice_number }} + +
+
+
diff --git a/src/app/modules/saleInvoices/views/single.component.ts b/src/app/modules/saleInvoices/views/single.component.ts index bc4ea6f..0ec3ef3 100644 --- a/src/app/modules/saleInvoices/views/single.component.ts +++ b/src/app/modules/saleInvoices/views/single.component.ts @@ -1,13 +1,15 @@ -import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component'; +import { SaleInvoiceSingleInfoCardComponent } from '@/shared/components/invoices/sale-invoice-single-info-card.component'; import pageParamsUtils from '@/utils/page-params.utils'; import { Component, computed, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { ButtonDirective } from 'primeng/button'; +import { Card } from 'primeng/card'; import { PublicSaleInvoiceStore } from '../store/main.store'; @Component({ selector: 'public-saleInvoice', templateUrl: './single.component.html', - imports: [SharedSaleInvoiceSingleViewComponent], + imports: [SaleInvoiceSingleInfoCardComponent, Card, ButtonDirective], }) export class PublicSaleInvoiceComponent { private readonly route = inject(ActivatedRoute); @@ -22,4 +24,14 @@ export class PublicSaleInvoiceComponent { ngOnInit() { this.store.getData(this.invoiceId()); } + + async share() { + if (navigator.share) { + await navigator.share({ + title: window.document.title, + text: 'برای مشاهده صورت‌حساب اینجا کلیک کنید.', + url: window.location.href, + }); + } + } } diff --git a/src/app/shared/components/dialog/light-bottomsheet.component.html b/src/app/shared/components/dialog/light-bottomsheet.component.html index b045bb5..d6f8d22 100644 --- a/src/app/shared/components/dialog/light-bottomsheet.component.html +++ b/src/app/shared/components/dialog/light-bottomsheet.component.html @@ -20,7 +20,9 @@ }
- + @if (contentRendered) { + + }
diff --git a/src/app/shared/components/dialog/light-bottomsheet.component.ts b/src/app/shared/components/dialog/light-bottomsheet.component.ts index 0546d4e..bc6fca9 100644 --- a/src/app/shared/components/dialog/light-bottomsheet.component.ts +++ b/src/app/shared/components/dialog/light-bottomsheet.component.ts @@ -105,10 +105,12 @@ 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; + @Input() closeUnmountDelay = 20; @Output() onHide = new EventEmitter(); rendered = false; + contentRendered = false; + private isClosing = false; private closeTimer: ReturnType | null = null; private originalParent: Node | null = null; @@ -127,14 +129,16 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha this.originalParent = host.parentNode; this.renderer.appendChild(this.document.body, host); this.rendered = this.visible; + this.contentRendered = this.visible; this.syncZIndex(); this.toggleBodyScrollLock(this.visible); } ngOnChanges(changes: SimpleChanges) { - if (changes['visible'] && this.visible) { + if (changes['visible'] && this.visible && !this.isClosing) { this.clearCloseTimer(); this.rendered = true; + this.contentRendered = true; this.syncZIndex(); } if (changes['visible']) { @@ -159,18 +163,20 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha } onVisibilityChange(nextValue: boolean) { - this.visible = nextValue; - this.visibleChange.emit(nextValue); - this.toggleBodyScrollLock(nextValue); if (nextValue) { + this.visible = nextValue; + this.isClosing = false; this.clearCloseTimer(); this.rendered = true; + this.contentRendered = true; this.syncZIndex(); } if (!nextValue) { + this.isClosing = true; this.scheduleUnmount(); - this.onHide.emit(); } + this.visibleChange.emit(nextValue); + this.toggleBodyScrollLock(nextValue); } onMaskClick() { @@ -217,12 +223,14 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha } private scheduleUnmount() { + if (!this.visible) { + this.isClosing = false; + this.contentRendered = false; + this.rendered = false; + this.onHide.emit(); + } this.clearCloseTimer(); - this.closeTimer = setTimeout(() => { - if (!this.visible) { - this.rendered = false; - } - }, this.closeUnmountDelay); + this.closeTimer = setTimeout(() => {}, this.closeUnmountDelay); } private clearCloseTimer() { diff --git a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html index b84e37b..ae372d5 100644 --- a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html +++ b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html @@ -1,10 +1,16 @@ -
+
- @if (backRoute) { - + @if (backRoute || onBackClick) { + } -
{{ pageTitle }}
+
{{ pageTitle }}
@if (actions) { diff --git a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts index adf82ef..a7f1712 100644 --- a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts +++ b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts @@ -1,6 +1,6 @@ import { Maybe } from '@/core'; import { NgTemplateOutlet } from '@angular/common'; -import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; +import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; import { RouterLink, UrlTree } from '@angular/router'; import { Button } from 'primeng/button'; @@ -12,6 +12,7 @@ import { Button } from 'primeng/button'; export class InnerPagesHeaderComponent { @Input() pageTitle!: string; @Input() backRoute?: UrlTree | string | any[]; + @Output() onBackClick = new EventEmitter(); @ContentChild('actions', { static: true }) actions?: Maybe>; constructor() {} } diff --git a/src/app/shared/components/invoices/correctionForm/form.component.html b/src/app/shared/components/invoices/correctionForm/form.component.html index 4243126..78342b7 100644 --- a/src/app/shared/components/invoices/correctionForm/form.component.html +++ b/src/app/shared/components/invoices/correctionForm/form.component.html @@ -41,7 +41,7 @@ }
- + diff --git a/src/app/shared/components/invoices/correctionForm/form.component.ts b/src/app/shared/components/invoices/correctionForm/form.component.ts index fe70424..a4968de 100644 --- a/src/app/shared/components/invoices/correctionForm/form.component.ts +++ b/src/app/shared/components/invoices/correctionForm/form.component.ts @@ -48,6 +48,12 @@ export class SharedCorrectionFormComponent extends AbstractForm< itemDrafts: Record = {}; mappedInitialItems: TCorrectionItemPayload[] = []; + totalPriceInfo = { + totalBaseAmount: 0, + discountAmount: 0, + taxAmount: 0, + totalAmount: 0, + }; showItemEditSheet = signal(false); activeItemIndex = signal(null); @@ -57,6 +63,7 @@ export class SharedCorrectionFormComponent extends AbstractForm< ); this.itemDrafts = {}; this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item)); + this.recalculateTotalPriceInfo(); this.showItemEditSheet.set(false); this.activeItemIndex.set(null); } @@ -91,9 +98,9 @@ export class SharedCorrectionFormComponent extends AbstractForm< good_id: item.good_id, unit_price: Number(item.unit_price || 0), quantity: Number(item.quantity || 0), - discount_amount: Number(item.discount || 0), + discount_amount: Number(item.discount_amount || 0), total_amount: Number(item.total_amount || 0), - tax_amount: 0, + tax_amount: Number(item.tax_amount || 0), base_total_amount: Number(item.unit_price || 0) * Number(item.quantity || 0), measure_unit: (item.good_snapshot?.measure_unit || {}) as ISummary, }; @@ -158,9 +165,6 @@ export class SharedCorrectionFormComponent extends AbstractForm< 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); @@ -178,6 +182,10 @@ export class SharedCorrectionFormComponent extends AbstractForm< ); } + private recalculateTotalPriceInfo() { + this.totalPriceInfo = this.getTotalPriceInfo(); + } + saveActiveItemDraft( goldForm?: SharedGoldPayloadFormComponent, standardForm?: SharedStandardPayloadFormComponent @@ -200,6 +208,7 @@ export class SharedCorrectionFormComponent extends AbstractForm< id: item.id, pricing_model: item.good_snapshot?.pricing_model || '', }; + this.recalculateTotalPriceInfo(); this.backToList(); } diff --git a/src/app/shared/components/invoices/sale-invoice-full-response.model.ts b/src/app/shared/components/invoices/sale-invoice-full-response.model.ts index c85641b..67f8aab 100644 --- a/src/app/shared/components/invoices/sale-invoice-full-response.model.ts +++ b/src/app/shared/components/invoices/sale-invoice-full-response.model.ts @@ -40,12 +40,13 @@ export interface IInvoiceItem { measure_unit_text: string; sku_code: string; unit_price: string; - discount: string; + discount_amount: string; total_amount: string; notes?: string; payload: Payload; good_snapshot: Goodsnapshot; good: GoodSummary; + tax_amount: string; } interface GoodSummary { name: string; @@ -132,7 +133,12 @@ interface Pos extends ISummary { } interface Complex extends ISummary { - business_activity: ISummary; + business_activity: BusinessActivity; +} + +interface BusinessActivity extends ISummary { + economic_code: string; + guild: ISummary; } interface ConsumerAccount { diff --git a/src/app/shared/components/invoices/sale-invoice-single-info-card.component.html b/src/app/shared/components/invoices/sale-invoice-single-info-card.component.html new file mode 100644 index 0000000..5fcc34c --- /dev/null +++ b/src/app/shared/components/invoices/sale-invoice-single-info-card.component.html @@ -0,0 +1,93 @@ +@if (invoice) { + +
+
+ + + + + + + + + +
+ +
+
+ اطلاعات مالی +
+ + + +
+ + اطلاعات پرداخت +
+ + + + + @for (payment of invoice.payments; track $index) { + + } +
+ + اطلاعات صادر کننده +
+ + + + @if (invoice.consumer_account?.account?.username) { + + } +
+ + @if (variant !== 'customer') { + اطلاعات مشتری + @if (invoice.customer) { +
+ @if (invoice.customer.type === 'INDIVIDUAL') { + + + + + + + } @else { + + + + + + } +
+ } @else if (invoice.unknown_customer) { +
+ + + +
+ } @else { +

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

+ } + } + کالاهای خریداری شده + + + @if (!item.discount_amount) { + + } + + +
+
+} diff --git a/src/app/shared/components/invoices/sale-invoice-single-info-card.component.ts b/src/app/shared/components/invoices/sale-invoice-single-info-card.component.ts new file mode 100644 index 0000000..b4cd6ec --- /dev/null +++ b/src/app/shared/components/invoices/sale-invoice-single-info-card.component.ts @@ -0,0 +1,104 @@ +import { Maybe } from '@/core'; +import { + CatalogInvoiceTypeTagComponent, + CatalogTaxProviderStatusTagComponent, +} from '@/shared/catalog'; +import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; +import { PriceMaskDirective } from '@/shared/directives'; +import { Component, Input } from '@angular/core'; +import { Card } from 'primeng/card'; +import { Divider } from 'primeng/divider'; +import { KeyValueComponent } from '../key-value.component/key-value.component'; +import { IColumn, PageDataListComponent } from '../pageDataList/page-data-list.component'; +import { ISaleInvoiceFullResponse } from './sale-invoice-full-response.model'; + +export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; + +@Component({ + selector: 'shared-sale-invoice-single-info-card', + templateUrl: './sale-invoice-single-info-card.component.html', + imports: [ + Card, + KeyValueComponent, + CatalogInvoiceTypeTagComponent, + CatalogTaxProviderStatusTagComponent, + Divider, + CatalogInvoiceSettlementTypeTagComponent, + PageDataListComponent, + PriceMaskDirective, + ], +}) +export class SaleInvoiceSingleInfoCardComponent { + @Input({ required: true }) loading!: boolean; + @Input() invoice!: Maybe; + @Input() variant: TSaleInvoiceSingleViewVariant = 'full'; + + columns: IColumn[] = [ + { + field: 'name', + header: 'عنوان', + type: 'nested', + nestedOption: { + path: 'good_snapshot.name', + }, + }, + { + field: 'sku_code', + header: 'شناسه کالا', + }, + { + field: 'unit_price', + header: 'قیمت واحد', + type: 'price', + }, + { + field: 'quantity', + header: 'مقدار', + customDataModel(item) { + return `${item.quantity} ${item.measure_unit_text}`; + }, + }, + { + field: 'commission', + header: 'کارمزد', + type: 'nested', + nestedOption: { + path: 'payload.commission', + type: 'price', + }, + }, + { + field: 'wages', + header: 'اجرت', + type: 'nested', + nestedOption: { + path: 'payload.wages', + type: 'price', + }, + }, + { + field: 'profit', + header: 'سود', + type: 'nested', + nestedOption: { + path: 'payload.profit', + type: 'price', + }, + }, + { + field: 'discount_amount', + header: 'مبلغ تخفیف', + type: 'price', + }, + { + field: 'tax_amount', + header: 'مبلغ مالیات', + type: 'price', + }, + { + field: 'total_amount', + header: 'مبلغ قابل پرداخت', + type: 'price', + }, + ]; +} 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 9b2947a..c7b4c8b 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,103 +3,14 @@ } @else if (!invoice) { } @else { - + +
- -
-
- - - - - - - - - -
- -
-
- اطلاعات مالی -
- - - -
- - اطلاعات پرداخت -
- - - - - @for (payment of invoice.payments; track $index) { - - } -
- - اطلاعات صادر کننده -
- - - - @if (invoice.consumer_account?.account?.username) { - - } -
- - @if (variant !== 'customer') { - اطلاعات مشتری - @if (invoice.customer) { -
- @if (invoice.customer.type === 'INDIVIDUAL') { - - - - - - - } @else { - - - - - - } -
- } @else if (invoice.unknown_customer) { -
- - - -
- } @else { -

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

- } - } - کالاهای خریداری شده - - - @if (!item.discount_amount) { - - } - - -
-
+
@if (moreActionMenuItems().length) { @@ -132,4 +43,14 @@ [invoiceDate]="invoice.invoice_date" (onSubmit)="correctionSubmit($event)" /> + +
+ + برای امکان دسترسی به صورت‌حساب به صورت آنلاین، کد QR را اسکن کنید. + + + + +
+
} 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 aea40ea..481e059 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 @@ -1,26 +1,17 @@ import { Maybe } from '@/core'; import { NativeBridgeService } from '@/core/services'; +import { NavigationService } from '@/core/services/navigation.service'; import { ToastService } from '@/core/services/toast.service'; import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service'; import { IPosReturnFromSaleRequest } from '@/domains/pos/modules/saleInvoices/models/returnFromSale'; import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { PosInfoStore } from '@/domains/pos/store'; -import { - CatalogInvoiceTypeTagComponent, - CatalogTaxProviderStatusTagComponent, - TspProviderResponseStatus, -} from '@/shared/catalog'; -import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; -import { KeyValueComponent, SharedLightBottomsheetComponent } from '@/shared/components'; +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 { - IColumn, - PageDataListComponent, -} from '@/shared/components/pageDataList/page-data-list.component'; -import { PriceMaskDirective } from '@/shared/directives'; import { UikitEmptyStateComponent } from '@/uikit'; -import { formatWithCurrency } from '@/utils'; +import { formatJalali, formatWithCurrency } from '@/utils'; import { Component, computed, @@ -33,12 +24,10 @@ import { TemplateRef, ViewChild, } from '@angular/core'; -import { UrlTree } from '@angular/router'; +import { Router } from '@angular/router'; +import { QRCodeComponent } from 'angularx-qrcode'; import { Button, ButtonDirective } from 'primeng/button'; import { Card } from 'primeng/card'; -import { Dialog } from 'primeng/dialog'; -import { Divider } from 'primeng/divider'; -import { ProgressSpinner } from 'primeng/progressspinner'; import { TableModule } from 'primeng/table'; import { Observable } from 'rxjs'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; @@ -46,8 +35,11 @@ import { InnerPagesHeaderComponent } from '../innerPagesHeader/inner-pages-heade import { SharedCorrectionFormComponent } from './correctionForm'; import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models'; import { SharedReturnFormComponent } from './returnForm/form.component'; +import { + SaleInvoiceSingleInfoCardComponent, + TSaleInvoiceSingleViewVariant, +} from './sale-invoice-single-info-card.component'; -export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; type TActionMenuItem = { label: string; icon: string; @@ -60,24 +52,17 @@ type TActionMenuItem = { selector: 'shared-sale-invoice-single-view', templateUrl: './sale-invoice-single-view.component.html', imports: [ - KeyValueComponent, - Divider, - PageDataListComponent, TableModule, PageLoadingComponent, UikitEmptyStateComponent, - PriceMaskDirective, - CatalogTaxProviderStatusTagComponent, - CatalogInvoiceTypeTagComponent, Button, - CatalogInvoiceSettlementTypeTagComponent, SharedLightBottomsheetComponent, - ProgressSpinner, SharedReturnFormComponent, SharedCorrectionFormComponent, - Dialog, ButtonDirective, InnerPagesHeaderComponent, + SaleInvoiceSingleInfoCardComponent, + QRCodeComponent, Card, ], }) @@ -88,11 +73,13 @@ export class SharedSaleInvoiceSingleViewComponent { //TODO: below service Must be transform from pos to shared. private readonly service = inject(PosConfigPrintService); private readonly confirmationService = inject(AppConfirmationService); + private readonly router = inject(Router); + private readonly navigationService = inject(NavigationService); @Input({ required: true }) loading!: boolean; @Input() invoice!: Maybe; @Input() variant: TSaleInvoiceSingleViewVariant = 'full'; - @Input() backRoute?: UrlTree | string | any[]; + @Input() backRoute?: string; @Input() sendToTspLoading: boolean = false; @Input() resendToTspLoading: boolean = false; @Input() inquiryLoading: boolean = false; @@ -109,6 +96,14 @@ export class SharedSaleInvoiceSingleViewComponent { moreActionMenuItems = signal([]); showCorrectionForm = signal(false); showReturnFromSaleForm = signal(false); + showQrCodeDialog = signal(false); + + publicInvoiceRoute = computed(() => { + if (this.invoice) { + return `${window.location.origin}/sale-invoices/${this.invoice.id}`; + } + return ''; + }); showErrors = () => {}; inquiry = () => { @@ -231,39 +226,39 @@ export class SharedSaleInvoiceSingleViewComponent { preparePrint = async () => { if (this.invoice) { - const mustPrintConfig = await this.service.get(); + const mustPrintConfig = await this.service.getVisibleItems(); const defaultPrintItems = [ { title: 'اطلاعات صورت‌حساب', items: [ + { + label: 'شماره', + value: this.invoice.invoice_number.toString(), + show: true, + }, { label: 'شماره منحصر به فرد مالیاتی', - value: 'this.invoice', + value: this.invoice.tax_id || '-', show: true, }, { - label: 'شماره صورت‌حساب', - value: this.invoice.code, - show: true, - }, - { - label: 'موضوع صورت‌حساب', - value: 'this.invoice', - show: mustPrintConfig.invoice_template ?? true, - }, - { - label: 'نوع صورت‌حساب', - value: 'this.invoice', + label: 'نوع', + value: this.invoice.type.translate, show: mustPrintConfig.invoice_template, }, { - label: 'تاریخ و ساعت', - value: this.invoice.invoice_date, + 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: 'وضعیت صورت‌حساب مالیاتی', + label: 'وضعیت صدور', value: this.invoice.status.translate, show: mustPrintConfig.show_payment_info ?? true, }, @@ -273,23 +268,23 @@ export class SharedSaleInvoiceSingleViewComponent { title: `اطلاعات فروشنده`, items: [ { - label: 'عنوان فروشگاه', + label: 'فروشگاه', value: this.invoice.pos.complex.business_activity.name, show: mustPrintConfig.business_name, }, { - label: 'عنوان شعبه', + label: 'شعبه', value: this.invoice.pos.complex.name, show: mustPrintConfig.complex_name, }, { - label: 'عنوان پایانه‌ فروش', + label: 'پایانه‌ فروش', value: this.invoice.pos.name, show: mustPrintConfig.pos_name, }, { 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), @@ -328,59 +323,67 @@ export class SharedSaleInvoiceSingleViewComponent { items: [ { label: 'شرح', - value: item.good.name, - show: mustPrintConfig.items?.name, + value: item.good_snapshot.name, + show: mustPrintConfig.item_name ?? true, }, { label: 'شناسه کالا / خدمت', value: item.sku_code, - show: mustPrintConfig.items?.sku, + show: mustPrintConfig.item_sku ?? true, }, { label: 'تعداد / مقدار', value: `${item.quantity} ${item.measure_unit_text}`, - show: mustPrintConfig.items?.quantity, + show: mustPrintConfig.item_quantity ?? true, }, { label: 'قیمت واحد', value: formatWithCurrency(item.unit_price), - show: mustPrintConfig.items?.base_amount, + show: mustPrintConfig.item_base_amount ?? true, }, { label: 'اجرت', value: formatWithCurrency(item.payload.wages), - show: mustPrintConfig.items?.wage && item.good_snapshot.pricing_model === 'GOLD', + show: + (mustPrintConfig.item_wage ?? true) && + item.good_snapshot.pricing_model === 'GOLD', }, { label: 'سود', - value: formatWithCurrency(item.payload.wages), - show: mustPrintConfig.items?.profit && item.good_snapshot.pricing_model === 'GOLD', + value: formatWithCurrency(item.payload.profit), + show: + (mustPrintConfig.item_profit ?? true) && + item.good_snapshot.pricing_model === 'GOLD', }, { label: 'حق‌العمل', value: formatWithCurrency(item.payload.commission), show: - mustPrintConfig.items?.commission && item.good_snapshot.pricing_model === 'GOLD', + (mustPrintConfig.item_commission ?? true) && + item.good_snapshot.pricing_model === 'GOLD', }, { label: 'مالیات بر ارزش افزوده', - value: 'item.good.tax', - show: mustPrintConfig.items?.tax, + value: formatWithCurrency(item.tax_amount), + show: mustPrintConfig.item_tax ?? true, }, { label: 'تخفیف', - value: formatWithCurrency(item.discount), - show: mustPrintConfig.items?.discount, + value: formatWithCurrency(item.discount_amount), + show: mustPrintConfig.item_discount ?? true, }, { label: 'مبلغ کل', value: formatWithCurrency(item.total_amount), - show: mustPrintConfig.items?.total_amount, + show: mustPrintConfig.item_total_amount ?? true, }, ], }); } } + + console.log(defaultPrintItems); + return defaultPrintItems; } return null; @@ -389,6 +392,7 @@ export class SharedSaleInvoiceSingleViewComponent { printInvoice = async () => { try { const printItems = await this.preparePrint(); + if (printItems) { this.nativeBridge.print(printItems); } @@ -399,75 +403,6 @@ export class SharedSaleInvoiceSingleViewComponent { } }; - columns: IColumn[] = [ - { - field: 'name', - header: 'عنوان', - type: 'nested', - nestedOption: { - path: 'good_snapshot.name', - }, - }, - { - field: 'sku_code', - header: 'شناسه کالا', - }, - { - field: 'unit_price', - header: 'قیمت واحد', - type: 'price', - }, - { - field: 'quantity', - header: 'مقدار', - customDataModel(item) { - return `${item.quantity} ${item.measure_unit_text}`; - }, - }, - { - field: 'commission', - header: 'کارمزد', - type: 'nested', - nestedOption: { - path: 'payload.commission', - type: 'price', - }, - }, - { - field: 'wages', - header: 'اجرت', - type: 'nested', - nestedOption: { - path: 'payload.wages', - type: 'price', - }, - }, - { - field: 'profit', - header: 'سود', - type: 'nested', - nestedOption: { - path: 'payload.profit', - type: 'price', - }, - }, - { - field: 'discount_amount', - header: 'مبلغ تخفیف', - type: 'price', - }, - { - field: 'tax_amount', - header: 'مبلغ مالیات', - type: 'price', - }, - { - field: 'total_amount', - header: 'مبلغ قابل پرداخت', - type: 'price', - }, - ]; - refresh() { this.onRefresh.emit(); } @@ -503,10 +438,10 @@ export class SharedSaleInvoiceSingleViewComponent { const quantity = Number(item.quantity || 0); const unit_price = Number(source.unit_price || 0); - const discount_amount = Number(source.discount || 0); + const discount_amount = Number(source.discount_amount || 0); const base_total_amount = unit_price * quantity; const total_amount = Number(source.total_amount || 0); - const tax_amount = Number(source.payload?.wages || 0); + const tax_amount = Number(source.tax_amount || 0); return { good_id: source.good_id, @@ -537,4 +472,11 @@ export class SharedSaleInvoiceSingleViewComponent { returnSubmit(event: ReturnFromSaleFormValue) { this.sendReturnFromSale(this.mapReturnFromSaleRequest(event)); } + + backToPrevPage() { + this.navigationService.back(this.backRoute); + } + showQrCode() { + this.showQrCodeDialog.set(true); + } } diff --git a/src/app/shared/components/posPayment/gold-form.component.html b/src/app/shared/components/posPayment/gold-form.component.html index f6f7f00..5ab907b 100644 --- a/src/app/shared/components/posPayment/gold-form.component.html +++ b/src/app/shared/components/posPayment/gold-form.component.html @@ -60,7 +60,7 @@ [discountAmount]="form.controls.discount_amount.value || 0" [taxAmount]="taxAmount()" /> @if (!isCorrection) { - + }
diff --git a/src/app/shared/components/posPayment/standard-form.component.html b/src/app/shared/components/posPayment/standard-form.component.html index 39fc798..bcaf0da 100644 --- a/src/app/shared/components/posPayment/standard-form.component.html +++ b/src/app/shared/components/posPayment/standard-form.component.html @@ -25,7 +25,7 @@ [discountAmount]="discountAmount()" [taxAmount]="taxAmount()" /> @if (!isCorrection) { - + } diff --git a/src/app/shared/components/seasonPicker/season-picker-dialog.component.html b/src/app/shared/components/seasonPicker/season-picker-dialog.component.html index deaee87..9c9a66f 100644 --- a/src/app/shared/components/seasonPicker/season-picker-dialog.component.html +++ b/src/app/shared/components/seasonPicker/season-picker-dialog.component.html @@ -2,13 +2,14 @@
انتخاب سال - + appendTo="body" + class="w-full" + [options]="years" + optionLabel="year" + optionValue="value"> +
diff --git a/src/app/shared/components/seasonPicker/season-picker-dialog.component.ts b/src/app/shared/components/seasonPicker/season-picker-dialog.component.ts index 98bea63..639a199 100644 --- a/src/app/shared/components/seasonPicker/season-picker-dialog.component.ts +++ b/src/app/shared/components/seasonPicker/season-picker-dialog.component.ts @@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common'; import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ButtonDirective, ButtonSeverity } from 'primeng/button'; +import { Select } from 'primeng/select'; import { SharedLightBottomsheetComponent } from '../dialog/light-bottomsheet.component'; type SeasonKey = 0 | 1 | 2 | 3; @@ -23,6 +24,7 @@ interface SeasonItem { SharedLightBottomsheetComponent, ButtonDirective, UikitLabelComponent, + Select, ], }) export class SeasonPickerDialogComponent extends AbstractDialog { diff --git a/src/app/shared/components/seasonPicker/season-picker.component.html b/src/app/shared/components/seasonPicker/season-picker.component.html index b3f505e..dce6dc7 100644 --- a/src/app/shared/components/seasonPicker/season-picker.component.html +++ b/src/app/shared/components/seasonPicker/season-picker.component.html @@ -1,9 +1,21 @@
- +
{{ currentSeason() }}
- +
@if (isOpen()) { diff --git a/src/app/shared/components/seasonPicker/season-picker.component.ts b/src/app/shared/components/seasonPicker/season-picker.component.ts index bbab297..d772b6b 100644 --- a/src/app/shared/components/seasonPicker/season-picker.component.ts +++ b/src/app/shared/components/seasonPicker/season-picker.component.ts @@ -18,7 +18,7 @@ interface SeasonItem { imports: [CommonModule, Button, SeasonPickerDialogComponent], }) export class SeasonPickerComponent implements OnInit { - @Input() minYear = 1390; + @Input() minYear = 1404; @Input() maxYear = Number(nowJalali(JALALI_DATE_FORMATS.YEAR)); @Input() value!: Date; @@ -66,13 +66,29 @@ export class SeasonPickerComponent implements OnInit { this.isOpen.set(false); } + canGoPrev(): boolean { + const index = this.selectedSeason(); + const currentYear = this.selectedYear(); + const nextSeason = index === 0 ? 3 : ((index - 1) as SeasonKey); + const nextYear = index === 0 ? currentYear - 1 : currentYear; + return this.isWithinBounds(nextYear, nextSeason); + } + + canGoNext(): boolean { + const index = this.selectedSeason(); + const currentYear = this.selectedYear(); + const nextSeason = index === 3 ? 0 : ((index + 1) as SeasonKey); + const nextYear = index === 3 ? currentYear + 1 : currentYear; + return this.isWithinBounds(nextYear, nextSeason); + } + prevSeason(): void { const index = this.selectedSeason(); const currentYear = this.selectedYear(); const nextSeason = index === 0 ? 3 : ((index - 1) as SeasonKey); const nextYear = index === 0 ? currentYear - 1 : currentYear; - // if (!this.isWithinBounds(nextYear, nextSeason)) return; + if (!this.isWithinBounds(nextYear, nextSeason)) return; this.selectedYear.set(nextYear); this.selectedSeason.set(nextSeason); @@ -85,14 +101,19 @@ export class SeasonPickerComponent implements OnInit { const nextSeason = index === 3 ? 0 : ((index + 1) as SeasonKey); const nextYear = index === 3 ? currentYear + 1 : currentYear; + if (!this.isWithinBounds(nextYear, nextSeason)) return; + this.selectedYear.set(nextYear); this.selectedSeason.set(nextSeason); this.emitValue(); } submitPicker(value: { year: number; season: number }): void { + const season = value.season as SeasonKey; + if (!this.isWithinBounds(value.year, season)) return; + this.selectedYear.set(value.year); - this.selectedSeason.set(value.season as SeasonKey); + this.selectedSeason.set(season); this.emitValue(); } diff --git a/src/app/utils/price-mask.utils.ts b/src/app/utils/price-mask.utils.ts index 28acdc3..d828365 100644 --- a/src/app/utils/price-mask.utils.ts +++ b/src/app/utils/price-mask.utils.ts @@ -61,7 +61,7 @@ export function formatWithCurrency( currency: string | null | undefined = 'ریال', opts?: { locale?: string; useComma?: boolean; fraction?: number } ): string { - if (num === null) return '0 ریال'; + if (num === null || num === undefined) return '0 ریال'; const formatted = formatNumber(parseFloat(num + ''), opts); if (isInputHost) return formatted; if (currency && currency.trim()) return `${formatted} ${currency.trim()}`; diff --git a/src/environments/environment.tis.ts b/src/environments/environment.tis.ts index 612d694..02aff61 100644 --- a/src/environments/environment.tis.ts +++ b/src/environments/environment.tis.ts @@ -1,9 +1,9 @@ // TIS tenant environment configuration export const environment = { production: true, - apiBaseUrl: 'https://psp-api.shift-am.ir', + // apiBaseUrl: 'https://psp-api.shift-am.ir', // apiBaseUrl: 'http://192.168.128.73:5002', - // apiBaseUrl: 'http://192.168.0.162:5002', + apiBaseUrl: 'http://192.168.0.162:5002', // apiBaseUrl: 'http://localhost:5002', host: 'localhost', port: 5000,