create invoice settlementStatus catalog and set to single invoice details, update single invoice detail actions

This commit is contained in:
2026-05-31 17:12:43 +03:30
parent 4ec6143068
commit c271a36f7e
15 changed files with 242 additions and 17 deletions
@@ -1,5 +1,7 @@
import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state'; import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state';
import { ISaleInvoiceFullResponse } from '@/domains/consumer/models'; import { ISaleInvoiceFullResponse } from '@/domains/consumer/models';
import { TspProviderResponseStatus } from '@/shared/catalog';
import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { catchError, finalize } from 'rxjs'; import { catchError, finalize } from 'rxjs';
import { PosSaleInvoicesService } from '../services/main.service'; import { PosSaleInvoicesService } from '../services/main.service';
@@ -9,7 +11,10 @@ interface PosSaleInvoiceState extends EntityState<ISaleInvoiceFullResponse> {}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class PosSaleInvoiceStore extends EntityStore<ISaleInvoiceFullResponse, PosSaleInvoiceState> { export class PosSaleInvoiceStore extends EntityStore<
ISaleInvoiceFullResponse,
PosSaleInvoiceState
> {
private readonly service = inject(PosSaleInvoicesService); private readonly service = inject(PosSaleInvoicesService);
constructor() { constructor() {
@@ -29,13 +34,29 @@ export class PosSaleInvoiceStore extends EntityStore<ISaleInvoiceFullResponse, P
catchError((error) => { catchError((error) => {
this.setError(error); this.setError(error);
throw error; throw error;
}), })
) )
.subscribe((entity) => { .subscribe((entity) => {
this.patchState({ entity }); this.patchState({ entity });
}); });
} }
updateStatus(status: TspProviderResponseStatus) {
this.patchState({
entity: {
...this.entity()!,
status: { value: status, translate: taxProviderStatusTranslatorUtil(status) },
},
});
}
sendToTsp(invoceId: string) {
return this.service.sendToTsp(invoceId);
}
inquiry(invoceId: string) {
return this.service.getInquiry(invoceId);
}
override reset(): void { override reset(): void {
this.setState({ this.setState({
...defaultBaseStateData, ...defaultBaseStateData,
@@ -3,6 +3,9 @@
[loading]="loading()" [loading]="loading()"
[invoice]="invoice()" [invoice]="invoice()"
[backRoute]="backRoute()" [backRoute]="backRoute()"
variant="pos" [sendToTspLoading]="sendToTspLoading()"
/> [inquiryLoading]="inquiryLoading()"
(onSendToTsp)="sendToTsp()"
(onInquiry)="inquiry()"
variant="pos" />
</div> </div>
@@ -1,7 +1,10 @@
import { ToastService } from '@/core/services/toast.service';
import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util';
import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component'; import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component';
import pageParamsUtils from '@/utils/page-params.utils'; import pageParamsUtils from '@/utils/page-params.utils';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { finalize } from 'rxjs';
import { posSaleInvoicesNamedRoutes } from '../constants'; import { posSaleInvoicesNamedRoutes } from '../constants';
import { PosSaleInvoiceStore } from '../store/main.store'; import { PosSaleInvoiceStore } from '../store/main.store';
@@ -13,15 +16,102 @@ import { PosSaleInvoiceStore } from '../store/main.store';
export class PosSaleInvoiceComponent { export class PosSaleInvoiceComponent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly store = inject(PosSaleInvoiceStore); private readonly store = inject(PosSaleInvoiceStore);
private readonly toastService = inject(ToastService);
pageParams = computed(() => pageParamsUtils(this.route)); pageParams = computed(() => pageParamsUtils(this.route));
invoiceId = signal<string>(this.pageParams()['invoiceId']); invoiceId = signal<string>(this.pageParams()['invoiceId']);
inquiryLoading = signal(false);
sendToTspLoading = signal(false);
resendToTspLoading = signal(false);
correctionLoading = signal(false);
backFromSaleLoading = signal(false);
revokeLoading = signal(false);
readonly invoice = computed(() => this.store.entity()); readonly invoice = computed(() => this.store.entity());
readonly loading = computed(() => this.store.loading()); readonly loading = computed(() => this.store.loading());
readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!()); readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!());
sendToTsp() {
this.sendToTspLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.sendToTspLoading.set(false);
})
)
.subscribe({
next: (res) => {
this.toastService.success({ text: res.message || 'ارسال صورت‌حساب با موفقیت انجام شد.' });
this.store.updateStatus(res.status);
},
});
}
inquiry() {
this.inquiryLoading.set(true);
this.store
.inquiry(this.invoiceId())
.pipe(
finalize(() => {
this.inquiryLoading.set(false);
})
)
.subscribe({
next: (res) => {
this.toastService.success({
text: `وضعیت صورت‌حساب ${taxProviderStatusTranslatorUtil(res.status)} می‌باشد.`,
});
this.store.updateStatus(res.status);
},
});
}
resendToTsp() {
this.resendToTspLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.resendToTspLoading.set(false);
})
)
.subscribe();
}
correction() {
this.correctionLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.correctionLoading.set(false);
})
)
.subscribe();
}
backFromSale() {
this.backFromSaleLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.backFromSaleLoading.set(false);
})
)
.subscribe();
}
revoke() {
this.revokeLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.revokeLoading.set(false);
})
)
.subscribe();
}
ngOnInit() { ngOnInit() {
this.store.getData(this.invoiceId()); this.store.getData(this.invoiceId());
} }
@@ -9,7 +9,7 @@ import { Component, computed, EventEmitter, Input, Output } from '@angular/core'
imports: [CommonModule], imports: [CommonModule],
}) })
export class PosStatisticsInvoiceTypeCardComponent { export class PosStatisticsInvoiceTypeCardComponent {
@Input() type: 'all' | 'success' | 'failure' | 'not_sended' | 'pending' | 'not_original' = 'all'; @Input() type: 'all' | 'success' | 'failure' | 'not_send' | 'pending' | 'not_original' = 'all';
@Input() count?: number = 0; @Input() count?: number = 0;
@Input() totalAmount?: number = 0; @Input() totalAmount?: number = 0;
@Input() totalTaxAmount?: number = 0; @Input() totalTaxAmount?: number = 0;
@@ -48,7 +48,7 @@ export class PosStatisticsInvoiceTypeCardComponent {
bgColor: '#EF44441A', bgColor: '#EF44441A',
borderColor: '#EF4444', borderColor: '#EF4444',
}; };
case 'not_sended': case 'not_send':
return { return {
title: taxProviderStatusTranslatorUtil('NOT_SEND'), title: taxProviderStatusTranslatorUtil('NOT_SEND'),
icon: 'clock', icon: 'clock',
+1 -1
View File
@@ -3,7 +3,7 @@ export interface IPosStatisticsRawResponse {
success: IPosStatisticsItem; success: IPosStatisticsItem;
failure: IPosStatisticsItem; failure: IPosStatisticsItem;
pending: IPosStatisticsItem; pending: IPosStatisticsItem;
not_sended: IPosStatisticsItem; not_send: IPosStatisticsItem;
not_original: IPosStatisticsItem; not_original: IPosStatisticsItem;
} }
export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {} export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {}
@@ -30,12 +30,12 @@
[count]="item()?.failure?.count" [count]="item()?.failure?.count"
(onClick)="toInvoicesPage('failure')" /> (onClick)="toInvoicesPage('failure')" />
<pos-statistics-invoice-type-card <pos-statistics-invoice-type-card
type="not_sended" type="not_send"
[loading]="loading()" [loading]="loading()"
[totalAmount]="item()?.not_sended?.total_amount" [totalAmount]="item()?.not_send?.total_amount"
[totalTaxAmount]="item()?.not_sended?.total_tax" [totalTaxAmount]="item()?.not_send?.total_tax"
[count]="item()?.not_sended?.count" [count]="item()?.not_send?.count"
(onClick)="toInvoicesPage('not_sended')" /> (onClick)="toInvoicesPage('not_send')" />
<pos-statistics-invoice-type-card <pos-statistics-invoice-type-card
type="pending" type="pending"
[loading]="loading()" [loading]="loading()"
@@ -0,0 +1,3 @@
export * from './invoice-settlement-type-tag.component';
export * from './invoice-settlement-type-translator.util';
export * from './type';
@@ -0,0 +1 @@
<p-tag [severity]="severity" size="large" [value]="translate || label"></p-tag>
@@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';
import { Tag } from 'primeng/tag';
import taxProviderStatusTranslatorUtil from './invoice-settlement-type-translator.util';
import { InvoiceSettlementType } from './type';
@Component({
selector: 'catalog-invoice-settlement-type-tag',
templateUrl: './invoice-settlement-type-tag.component.html',
imports: [Tag],
})
export class CatalogInvoiceSettlementTypeTagComponent {
@Input({ required: true }) type!: InvoiceSettlementType;
@Input() translate!: string;
get severity() {
switch (this.type) {
case 'CASH':
return 'success';
case 'CREDIT':
return 'secondary';
case 'MIXED':
return 'warn';
default:
return 'secondary';
}
}
get label() {
return taxProviderStatusTranslatorUtil(this.type);
}
}
@@ -0,0 +1,14 @@
import { InvoiceSettlementType } from './type';
export default (status: InvoiceSettlementType) => {
switch (status) {
case 'CASH':
return 'نقدی';
case 'CREDIT':
return 'نسیه';
case 'MIXED':
return 'نقدی / نسیه';
default:
return 'نامشخص';
}
};
@@ -0,0 +1,8 @@
export const InvoiceSettlementType = {
CASH: 'CASH',
CREDIT: 'CREDIT',
MIXED: 'MIXED',
} as const;
export type InvoiceSettlementType =
(typeof InvoiceSettlementType)[keyof typeof InvoiceSettlementType];
@@ -1,6 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { InvoiceTypes, TspProviderResponseStatus } from '@/shared/catalog'; import { InvoiceTypes, TspProviderResponseStatus } from '@/shared/catalog';
import { InvoiceSettlementType } from '@/shared/catalog/invoiceSettlementType';
import { IEnumTranslate } from '@/shared/models/enum_translate.type'; import { IEnumTranslate } from '@/shared/models/enum_translate.type';
export interface ISaleInvoiceFullRawResponse { export interface ISaleInvoiceFullRawResponse {
@@ -24,6 +25,7 @@ export interface ISaleInvoiceFullRawResponse {
tax_id: Maybe<string>; tax_id: Maybe<string>;
reference_invoice: Maybe<string>; reference_invoice: Maybe<string>;
notes: Maybe<string>; notes: Maybe<string>;
settlement_type: IEnumTranslate<InvoiceSettlementType>;
} }
export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {} export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {}
@@ -3,6 +3,12 @@
} @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="لطفا صبر کنید">
<div class="flex h-[30svh] flex-col items-center justify-center gap-6">
<p-progressSpinner />
<span class="text-lg font-bold">در حال ارسال درخواست شما...</span>
</div>
</shared-light-bottomsheet>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات صورت‌حساب‌" [editable]="false" [backRoute]="backRoute"> <app-card-data cardTitle="اطلاعات صورت‌حساب‌" [editable]="false" [backRoute]="backRoute">
<ng-template #moreActions> <ng-template #moreActions>
@@ -26,11 +32,18 @@
<app-key-value label="توضیحات" [value]="invoice.notes || '----'" /> <app-key-value label="توضیحات" [value]="invoice.notes || '----'" />
</div> </div>
</div> </div>
<p-divider align="center"> اطلاعات پرداخت </p-divider> <p-divider align="center"> اطلاعات مالی</p-divider>
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3"> <div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
<app-key-value label="مبلغ تخفیف" [value]="invoice.discount_amount" type="price" /> <app-key-value label="مبلغ تخفیف" [value]="invoice.discount_amount" type="price" />
<app-key-value label="مبلغ مالیات" [value]="invoice.tax_amount" type="price" /> <app-key-value label="مبلغ مالیات" [value]="invoice.tax_amount" type="price" />
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
</div>
<p-divider align="center"> اطلاعات پرداخت</p-divider>
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
<app-key-value label="نحوه‌ی تسویه‌حساب">
<catalog-invoice-settlement-type-tag [type]="invoice.settlement_type.value" />
</app-key-value>
@for (payment of invoice.payments; track $index) { @for (payment of invoice.payments; track $index) {
<app-key-value <app-key-value
@@ -7,7 +7,12 @@ import {
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
CatalogTaxProviderStatusTagComponent, CatalogTaxProviderStatusTagComponent,
} from '@/shared/catalog'; } from '@/shared/catalog';
import { AppCardComponent, KeyValueComponent } from '@/shared/components'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import {
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 {
@@ -34,7 +39,9 @@ import { MenuItem } from 'primeng/api';
import { Button } from 'primeng/button'; import { Button } from 'primeng/button';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { Menu } from 'primeng/menu'; import { Menu } from 'primeng/menu';
import { ProgressSpinner } from 'primeng/progressspinner';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Observable } from 'rxjs';
import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service';
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
@@ -55,6 +62,9 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
Menu, Menu,
Button, Button,
CatalogInvoiceSettlementTypeTagComponent,
SharedLightBottomsheetComponent,
ProgressSpinner,
], ],
}) })
export class SharedSaleInvoiceSingleViewComponent { export class SharedSaleInvoiceSingleViewComponent {
@@ -69,8 +79,13 @@ export class SharedSaleInvoiceSingleViewComponent {
@Input() invoice!: Maybe<ISaleInvoiceFullResponse>; @Input() invoice!: Maybe<ISaleInvoiceFullResponse>;
@Input() variant: TSaleInvoiceSingleViewVariant = 'full'; @Input() variant: TSaleInvoiceSingleViewVariant = 'full';
@Input() backRoute?: UrlTree | string | any[]; @Input() backRoute?: UrlTree | string | any[];
@Input() sendToTspLoading: boolean = false;
@Input() resendToTspLoading: boolean = false;
@Input() inquiryLoading: boolean = false;
@Output() onRefresh = new EventEmitter<void>(); @Output() onRefresh = new EventEmitter<void>();
@Output() onSendToTsp = new EventEmitter<Observable<any>>();
@Output() onInquiry = new EventEmitter<Observable<any>>();
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>; @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
@@ -82,9 +97,21 @@ export class SharedSaleInvoiceSingleViewComponent {
this.showCorrection = this.showCorrection.bind(this); this.showCorrection = this.showCorrection.bind(this);
this.showBackFromSale = this.showBackFromSale.bind(this); this.showBackFromSale = this.showBackFromSale.bind(this);
this.printInvoice = this.printInvoice.bind(this); this.printInvoice = this.printInvoice.bind(this);
this.send = this.send.bind(this);
this.inquiry = this.inquiry.bind(this);
}
get actionLoading() {
return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading;
} }
showErrors() {} showErrors() {}
inquiry() {
this.onInquiry.emit();
}
send() {
this.onSendToTsp.emit();
}
showRevokeConfirmation() { showRevokeConfirmation() {
this.confirmationService.ask({ this.confirmationService.ask({
header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`,
@@ -112,6 +139,18 @@ export class SharedSaleInvoiceSingleViewComponent {
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[] = [ const actions: MenuItem[] = [
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: 'NOT_SEND',
command: this.send,
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: 'FISCAL_QUEUED',
command: this.inquiry,
},
{ {
label: 'دلایل خطا', label: 'دلایل خطا',
icon: 'pi pi-question', icon: 'pi pi-question',
+2 -2
View File
@@ -2,8 +2,8 @@
export const environment = { export const environment = {
production: true, 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.128.73:5002',
apiBaseUrl: 'http://localhost:5002', // apiBaseUrl: 'http://localhost:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,
enableLogging: false, enableLogging: false,