feat: implement invoice number sequence field and enhance tax provider status handling across components

This commit is contained in:
2026-05-03 16:08:26 +03:30
parent 83f124b910
commit 797aecd489
22 changed files with 160 additions and 34 deletions
@@ -14,7 +14,7 @@
<app-key-value label="تاریخ فاکتور" [value]="invoice.invoice_date" type="dateTime" /> <app-key-value label="تاریخ فاکتور" [value]="invoice.invoice_date" type="dateTime" />
<app-key-value label="تاریخ ایجاد فاکتور" [value]="invoice.created_at" type="dateTime" /> <app-key-value label="تاریخ ایجاد فاکتور" [value]="invoice.created_at" type="dateTime" />
<app-key-value label="وضعیت صدور به سامانه‌ی مودیان"> <app-key-value label="وضعیت صدور به سامانه‌ی مودیان">
<p-badge value="ارسال نشده" severity="danger" /> <catalog-tax-provider-status-tag [status]="invoice.status.value" [translate]="invoice.status.translate" />
</app-key-value> </app-key-value>
<div class="col-span-3"> <div class="col-span-3">
<app-key-value label="توضیحات" [value]="invoice.notes" /> <app-key-value label="توضیحات" [value]="invoice.notes" />
@@ -1,5 +1,6 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { NativeBridgeService } from '@/core/services'; import { NativeBridgeService } from '@/core/services';
import { CatalogTaxProviderStatusTagComponent } from '@/shared/catalog';
import { AppCardComponent, KeyValueComponent } from '@/shared/components'; import { AppCardComponent, KeyValueComponent } from '@/shared/components';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { import {
@@ -9,8 +10,15 @@ import {
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit'; import { UikitEmptyStateComponent } from '@/uikit';
import { getGoodUnitTypeProperties } from '@/utils'; import { getGoodUnitTypeProperties } from '@/utils';
import { Component, EventEmitter, inject, Input, Output, TemplateRef, ViewChild } from '@angular/core'; import {
import { Badge } from 'primeng/badge'; Component,
EventEmitter,
inject,
Input,
Output,
TemplateRef,
ViewChild,
} from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
@@ -24,7 +32,6 @@ export type TConsumerSaleInvoice = 'full' | 'customer' | 'pos';
imports: [ imports: [
AppCardComponent, AppCardComponent,
KeyValueComponent, KeyValueComponent,
Badge,
Divider, Divider,
ButtonDirective, ButtonDirective,
PageDataListComponent, PageDataListComponent,
@@ -32,6 +39,7 @@ export type TConsumerSaleInvoice = 'full' | 'customer' | 'pos';
PageLoadingComponent, PageLoadingComponent,
UikitEmptyStateComponent, UikitEmptyStateComponent,
PriceMaskDirective, PriceMaskDirective,
CatalogTaxProviderStatusTagComponent,
], ],
}) })
export class ConsumerSaleInvoiceSharedComponent { export class ConsumerSaleInvoiceSharedComponent {
@@ -1,4 +1,6 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { TspProviderResponseStatus } from '@/shared/catalog';
import { IEnumTranslate } from '@/shared/models/enum_translate.type';
export interface ISaleInvoiceFullRawResponse { export interface ISaleInvoiceFullRawResponse {
id: string; id: string;
@@ -13,6 +15,7 @@ export interface ISaleInvoiceFullRawResponse {
notes?: string; notes?: string;
customer?: Customer; customer?: Customer;
unknown_customer?: UnknownCustomer; unknown_customer?: UnknownCustomer;
status: IEnumTranslate<TspProviderResponseStatus>;
} }
export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {} export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {}
@@ -11,6 +11,7 @@
<field-economic-code [control]="form.controls.economic_code" /> <field-economic-code [control]="form.controls.economic_code" />
<field-fiscal-code [control]="form.controls.fiscal_id" /> <field-fiscal-code [control]="form.controls.fiscal_id" />
<field-partner-token [control]="form.controls.partner_token" /> <field-partner-token [control]="form.controls.partner_token" />
<field-invoice-number-sequence [control]="form.controls.invoice_number_sequence" [min]="1" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
</shared-dialog> </shared-dialog>
@@ -6,6 +6,7 @@ import {
PartnerTokenComponent, PartnerTokenComponent,
} from '@/shared/components'; } from '@/shared/components';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component'; import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { InvoiceNumberSequenceComponent } from '@/shared/components/fields/invoice_number_sequence.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { fieldControl } from '@/shared/constants/fields'; import { fieldControl } from '@/shared/constants/fields';
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
@@ -22,8 +23,9 @@ import { BusinessActivitiesService } from '../services/main.service';
FormFooterActionsComponent, FormFooterActionsComponent,
NameComponent, NameComponent,
EconomicCodeComponent, EconomicCodeComponent,
FiscalIdComponent,
PartnerTokenComponent, PartnerTokenComponent,
InvoiceNumberSequenceComponent,
FiscalIdComponent,
], ],
}) })
export class ConsumerBusinessActivityFormComponent extends AbstractFormDialog< export class ConsumerBusinessActivityFormComponent extends AbstractFormDialog<
@@ -38,6 +40,9 @@ export class ConsumerBusinessActivityFormComponent extends AbstractFormDialog<
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''), economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''),
fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''), fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''),
partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''), partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''),
invoice_number_sequence: fieldControl.invoice_number_sequence(
this.initialValues?.invoice_number_sequence || 1,
),
}); });
override submitForm(payload: IBusinessActivityRequest) { override submitForm(payload: IBusinessActivityRequest) {
@@ -10,10 +10,13 @@
(onRefresh)="refresh()" (onRefresh)="refresh()"
> >
</app-page-data-list> </app-page-data-list>
<consumer-businessActivity-form
@if (visibleForm()) {
<consumer-businessActivity-form
[(visible)]="visibleForm" [(visible)]="visibleForm"
[editMode]="editMode()" [editMode]="editMode()"
[businessId]="selectedItemForEdit()?.id || ''" [businessId]="selectedItemForEdit()?.id || ''"
[initialValues]="selectedItemForEdit() || undefined" [initialValues]="selectedItemForEdit() || undefined"
(onSubmit)="refresh()" (onSubmit)="refresh()"
/> />
}
@@ -8,6 +8,7 @@ export interface IBusinessActivityRawResponse {
partner_token: string; partner_token: string;
guild: ISummary; guild: ISummary;
created_at: string; created_at: string;
invoice_number_sequence: number;
license_info: LicenseInfo; license_info: LicenseInfo;
} }
export interface IBusinessActivityResponse extends IBusinessActivityRawResponse {} export interface IBusinessActivityResponse extends IBusinessActivityRawResponse {}
@@ -17,6 +18,7 @@ export interface IBusinessActivityRequest {
fiscal_id: string; fiscal_id: string;
partner_token: string; partner_token: string;
name: string; name: string;
invoice_number_sequence: number;
} }
interface LicenseInfo { interface LicenseInfo {
@@ -1,11 +1,7 @@
<p-card class="border border-surface-border p-0! relative overflow-visible"> <p-card class="border border-surface-border p-0! relative overflow-visible">
<div class="flex items-center gap-4 justify-between w-full"> <div class="flex items-center gap-4 justify-between w-full">
<span> {{ saleInvoice.invoice_number }} - {{ saleInvoice.code }} </span> <span> {{ saleInvoice.invoice_number }} - {{ saleInvoice.code }} </span>
<p-tag <catalog-tax-provider-status-tag [status]="saleInvoice.status.value" [translate]="saleInvoice.status.translate" />
[severity]="saleInvoice.status.value.toLowerCase() === 'not_send' ? 'warn' : 'success'"
size="large"
[value]="saleInvoice.status.translate"
></p-tag>
</div> </div>
<hr /> <hr />
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
@@ -1,4 +1,5 @@
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { CatalogTaxProviderStatusTagComponent } from '@/shared/catalog/taxProviderStatus';
import { KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core'; import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
@@ -13,7 +14,15 @@ import { PosSaleInvoicesService } from '../services/main.service';
@Component({ @Component({
selector: 'pos-saleInvoice-card', selector: 'pos-saleInvoice-card',
templateUrl: './sale-invoice-card.component.html', templateUrl: './sale-invoice-card.component.html',
imports: [Button, KeyValueComponent, Tag, Card, RouterLink, ButtonDirective], imports: [
Button,
KeyValueComponent,
Tag,
Card,
RouterLink,
ButtonDirective,
CatalogTaxProviderStatusTagComponent,
],
}) })
export class SaleInvoiceCardComponent { export class SaleInvoiceCardComponent {
private readonly service = inject(PosSaleInvoicesService); private readonly service = inject(PosSaleInvoicesService);
@@ -62,7 +71,7 @@ export class SaleInvoiceCardComponent {
getStatus() { getStatus() {
this.gettingStatusLoading.set(true); this.gettingStatusLoading.set(true);
this.service this.service
.refreshFiscalStatus(this.saleInvoice.id) .getInquiry(this.saleInvoice.id)
.pipe(finalize(() => this.gettingStatusLoading.set(false))) .pipe(finalize(() => this.gettingStatusLoading.set(false)))
.subscribe((res) => { .subscribe((res) => {
this.toastService.info({ this.toastService.info({
@@ -4,10 +4,10 @@ export const POS_SALE_INVOICES_API_ROUTES = {
list: () => baseUrl(), list: () => baseUrl(),
single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`, single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`,
fiscal: { fiscal: {
send: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/send`, send: (invoiceId: string) => `${baseUrl()}/${invoiceId}/send`,
retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`, retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/retry`,
status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`, status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`,
refreshStatus: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status/refresh`, getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`,
attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`, attempts: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/attempts`,
}, },
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ export interface IPosSaleInvoicesSummaryRawResponse {
total_amount: string; total_amount: string;
customer: Customer; customer: Customer;
fiscal: null; fiscal: null;
status: IEnumTranslate; status: IEnumTranslate<TspProviderResponseStatus>;
} }
export interface IPosSaleInvoicesSummaryResponse extends IPosSaleInvoicesSummaryRawResponse {} export interface IPosSaleInvoicesSummaryResponse extends IPosSaleInvoicesSummaryRawResponse {}
@@ -5,10 +5,10 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { POS_SALE_INVOICES_API_ROUTES } from '../constants'; import { POS_SALE_INVOICES_API_ROUTES } from '../constants';
import { import {
IPosSaleInvoicesFilterDto,
IPosSaleInvoiceFiscalActionResponse, IPosSaleInvoiceFiscalActionResponse,
IPosSaleInvoiceFiscalAttemptsResponse, IPosSaleInvoiceFiscalAttemptsResponse,
IPosSaleInvoiceFiscalStatusResponse, IPosSaleInvoiceFiscalStatusResponse,
IPosSaleInvoicesFilterDto,
IPosSaleInvoicesSummaryRawResponse, IPosSaleInvoicesSummaryRawResponse,
IPosSaleInvoicesSummaryResponse, IPosSaleInvoicesSummaryResponse,
} from '../models'; } from '../models';
@@ -19,7 +19,9 @@ export class PosSaleInvoicesService {
private apiRoutes = POS_SALE_INVOICES_API_ROUTES; private apiRoutes = POS_SALE_INVOICES_API_ROUTES;
getAll(query: IPosSaleInvoicesFilterDto = {}): Observable<IPaginatedResponse<IPosSaleInvoicesSummaryResponse>> { getAll(
query: IPosSaleInvoicesFilterDto = {},
): Observable<IPaginatedResponse<IPosSaleInvoicesSummaryResponse>> {
return this.http.get<IPaginatedResponse<IPosSaleInvoicesSummaryRawResponse>>( return this.http.get<IPaginatedResponse<IPosSaleInvoicesSummaryRawResponse>>(
this.apiRoutes.list(), this.apiRoutes.list(),
{ params: query as Record<string, string | number | boolean> }, { params: query as Record<string, string | number | boolean> },
@@ -31,25 +33,35 @@ export class PosSaleInvoicesService {
} }
sendFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> { sendFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> {
return this.http.post<IPosSaleInvoiceFiscalActionResponse>(this.apiRoutes.fiscal.send(invoiceId), {}); return this.http.post<IPosSaleInvoiceFiscalActionResponse>(
this.apiRoutes.fiscal.send(invoiceId),
{},
);
} }
retryFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> { retryFiscal(invoiceId: string): Observable<IPosSaleInvoiceFiscalActionResponse> {
return this.http.post<IPosSaleInvoiceFiscalActionResponse>(this.apiRoutes.fiscal.retry(invoiceId), {}); return this.http.post<IPosSaleInvoiceFiscalActionResponse>(
this.apiRoutes.fiscal.retry(invoiceId),
{},
);
} }
getFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> { getFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(this.apiRoutes.fiscal.status(invoiceId)); return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.status(invoiceId),
);
} }
refreshFiscalStatus(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> { getInquiry(invoiceId: string): Observable<IPosSaleInvoiceFiscalStatusResponse> {
return this.http.post<IPosSaleInvoiceFiscalStatusResponse>( return this.http.get<IPosSaleInvoiceFiscalStatusResponse>(
this.apiRoutes.fiscal.refreshStatus(invoiceId), this.apiRoutes.fiscal.getInquiry(invoiceId),
{}, {},
); );
} }
getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> { getFiscalAttempts(invoiceId: string): Observable<IPosSaleInvoiceFiscalAttemptsResponse> {
return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>(this.apiRoutes.fiscal.attempts(invoiceId)); return this.http.get<IPosSaleInvoiceFiscalAttemptsResponse>(
this.apiRoutes.fiscal.attempts(invoiceId),
);
} }
} }
@@ -163,7 +163,11 @@ export class AppLayout {
this.router.navigateByUrl('super_admin'); this.router.navigateByUrl('super_admin');
break; break;
case 'CONSUMER': case 'CONSUMER':
if (this.authService.currentAccount()?.role === 'OWNER')
this.router.navigateByUrl('consumer'); this.router.navigateByUrl('consumer');
else {
this.router.navigateByUrl('pos');
}
break; break;
case 'PARTNER': case 'PARTNER':
this.router.navigateByUrl('partner'); this.router.navigateByUrl('partner');
@@ -38,6 +38,9 @@ export abstract class AbstractForm<
ngOnInit() { ngOnInit() {
this.form.patchValue(this.initialValues ?? {}); this.form.patchValue(this.initialValues ?? {});
// Object.entries(this.form.controls).forEach(([key, control]) => {
// console.log(key, control);
// });
} }
ngOnChanges() { ngOnChanges() {
@@ -19,5 +19,5 @@ export const ENUMS_API_ROUTES = {
unitType: `${baseUrl}/unit_type`, unitType: `${baseUrl}/unit_type`,
goodPricingModel: `${baseUrl}/good_pricing_model`, goodPricingModel: `${baseUrl}/good_pricing_model`,
consumerRole: `${baseUrl}/consumer_role`, consumerRole: `${baseUrl}/consumer_role`,
fiscalResponseStatus: `${baseUrl}/fiscal_response_status`, fiscalResponseStatus: `${baseUrl}/tsp_provider_response_status`,
}; };
+1
View File
@@ -1,3 +1,4 @@
export * from './devices'; export * from './devices';
export * from './guild'; export * from './guild';
export * from './providers'; export * from './providers';
export * from './taxProviderStatus';
@@ -0,0 +1,2 @@
export * from './tax-provider-status-tag.component';
export * from './type';
@@ -0,0 +1 @@
<p-tag [severity]="severity" size="large" [value]="translate || label"></p-tag>
@@ -0,0 +1,43 @@
import { Component, Input } from '@angular/core';
import { Tag } from 'primeng/tag';
import { TspProviderResponseStatus } from './type';
@Component({
selector: 'catalog-tax-provider-status-tag',
templateUrl: './tax-provider-status-tag.component.html',
imports: [Tag],
})
export class CatalogTaxProviderStatusTagComponent {
@Input({ required: true }) status!: TspProviderResponseStatus;
@Input() translate!: string;
get severity() {
switch (this.status) {
case 'SUCCESS':
return 'success';
case 'FAILURE':
return 'danger';
case 'NOT_SEND':
return 'warn';
case 'QUEUED':
return 'info';
default:
return 'secondary';
}
}
get label() {
switch (this.status) {
case 'SUCCESS':
return 'موفق';
case 'FAILURE':
return 'ناموفق';
case 'NOT_SEND':
return 'ارسال نشده';
case 'QUEUED':
return 'در صف ارسال';
default:
return 'نامشخص';
}
}
}
@@ -0,0 +1,9 @@
export const TspProviderResponseStatus = {
SUCCESS: 'SUCCESS',
FAILURE: 'FAILURE',
NOT_SEND: 'NOT_SEND',
QUEUED: 'QUEUED',
} as const;
export type TspProviderResponseStatus =
(typeof TspProviderResponseStatus)[keyof typeof TspProviderResponseStatus];
@@ -0,0 +1,20 @@
import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component';
@Component({
selector: 'field-invoice-number-sequence',
template: `<app-input
label="شروع شماره فاکتور"
[control]="control"
[name]="name"
type="number"
[min]="min"
/>`,
imports: [ReactiveFormsModule, InputComponent],
})
export class InvoiceNumberSequenceComponent {
@Input({ required: true }) control = new FormControl<number>(1);
@Input() name = 'invoice_number_sequence';
@Input() min? = 1;
}
+5 -1
View File
@@ -3,7 +3,7 @@ import { nationalIdValidator } from '@/core/validators/national-id.validator';
import { ValidatorFn, Validators } from '@angular/forms'; import { ValidatorFn, Validators } from '@angular/forms';
const required = () => [Validators.required] as ValidatorFn[]; const required = () => [Validators.required] as ValidatorFn[];
type ControlConfig = [string, ValidatorFn[]]; type ControlConfig = [any, ValidatorFn[]];
export const fieldControl = { export const fieldControl = {
name: (value = '', isRequired = true): ControlConfig => [value, isRequired ? required() : []], name: (value = '', isRequired = true): ControlConfig => [value, isRequired ? required() : []],
@@ -78,6 +78,10 @@ export const fieldControl = {
value, value,
isRequired ? required() : [], isRequired ? required() : [],
], ],
invoice_number_sequence: (value = 1, isRequired = true): ControlConfig => [
value,
isRequired ? [Validators.required, Validators.min(1)] : [Validators.min(1)],
],
guild_id: (value = '', isRequired = true): ControlConfig => [value, isRequired ? required() : []], guild_id: (value = '', isRequired = true): ControlConfig => [value, isRequired ? required() : []],
license_starts_at: (value = '', isRequired = true): ControlConfig => [ license_starts_at: (value = '', isRequired = true): ControlConfig => [
value, value,