feat: add sale invoice card component with functionality for sending and retrieving invoice status

- Implemented sale invoice card component with HTML and TypeScript files.
- Added API routes for sale invoices including sending, retrying, and checking status.
- Created models for sale invoice fiscal actions and filters.
- Developed service for handling sale invoice operations.
- Introduced store for managing sale invoice state.
- Created views for listing and displaying single sale invoices.
- Added support for managing stock keeping units with forms and services.
- Implemented reusable select components for measure units and SKUs.
- Enhanced shared components for input fields including fiscal ID, SKU type, and VAT.
This commit is contained in:
2026-05-01 19:45:30 +03:30
parent 8104f1b7a7
commit 83f124b910
94 changed files with 1555 additions and 214 deletions
@@ -1,4 +1,5 @@
import { MenuItem } from 'primeng/api';
import { stockKeepingUnitsNamedRoutes } from '../modules/stockKeepingUnits/constants';
export const SUPER_ADMIN_MENU_ITEMS = [
{
items: [
@@ -7,45 +8,6 @@ export const SUPER_ADMIN_MENU_ITEMS = [
icon: 'pi pi-fw pi-home',
routerLink: ['/'],
},
],
},
{
label: 'مدیریت اطلاعات مرجع',
icon: 'pi pi-fw pi-cog',
items: [
{
label: 'اصناف',
icon: 'pi pi-fw pi-building',
routerLink: ['/super_admin/guilds'],
},
{
label: 'لایسنس‌ها',
icon: 'pi pi-fw pi-credit-card',
routerLink: ['/super_admin/licenses'],
},
{
label: 'برندهای دستگاه',
icon: 'pi pi-fw pi-list',
routerLink: ['/super_admin/device_brands'],
},
{
label: 'دستگاه‌ها',
icon: 'pi pi-fw pi-list',
routerLink: ['/super_admin/devices'],
},
],
},
{
label: 'مدیریت کاربران',
icon: 'pi pi-fw pi-cog',
items: [
{
label: 'کاربران',
icon: 'pi pi-fw pi-user',
routerLink: ['/super_admin/users'],
},
{
label: 'مشتریان',
icon: 'pi pi-fw pi-user',
@@ -64,4 +26,47 @@ export const SUPER_ADMIN_MENU_ITEMS = [
},
],
},
{
label: 'مدیریت اطلاعات مرجع',
icon: 'pi pi-fw pi-cog',
items: [
{
label: 'اصناف',
icon: 'pi pi-fw pi-building',
routerLink: ['/super_admin/guilds'],
},
{
label: 'شناسه‌ی کالاها',
icon: 'pi pi-fw pi-good',
routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()],
},
{
label: 'لایسنس‌ها',
icon: 'pi pi-fw pi-credit-card',
routerLink: ['/super_admin/licenses'],
},
{
label: 'برندهای دستگاه',
icon: 'pi pi-fw pi-list',
routerLink: ['/super_admin/device_brands'],
},
{
label: 'دستگاه‌ها',
icon: 'pi pi-fw pi-list',
routerLink: ['/super_admin/devices'],
},
],
},
{
label: 'تنظیمات',
icon: 'pi pi-fw pi-cog',
items: [
{
label: 'کاربران',
icon: 'pi pi-fw pi-user',
routerLink: ['/super_admin/users'],
},
],
},
] as MenuItem[];
@@ -9,7 +9,7 @@
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<field-name [control]="form.controls.name" />
<field-economic-code [control]="form.controls.economic_code" />
<field-fiscal-code [control]="form.controls.fiscal_code" />
<field-fiscal-code [control]="form.controls.fiscal_id" />
<field-partner-token [control]="form.controls.partner_token" />
<field-guild-id [control]="form.controls.guild_id" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
@@ -2,18 +2,18 @@
import { AbstractFormDialog } from '@/shared/abstractClasses';
import {
EconomicCodeComponent,
FiscalCodeComponent,
FiscalIdComponent,
GuildIdComponent,
NameComponent,
PartnerTokenComponent,
} from '@/shared/components';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { fieldControl } from '@/shared/constants/fields';
import { Component, inject, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { IBusinessActivityRequest, IBusinessActivityResponse } from '../../models';
import { AdminConsumerBusinessActivitiesService } from '../../services/businessActivities.service';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
@Component({
selector: 'superAdmin-consumer-businessActivities-form',
@@ -23,7 +23,7 @@ import { SharedDialogComponent } from '@/shared/components/dialog/dialog.compone
SharedDialogComponent,
NameComponent,
EconomicCodeComponent,
FiscalCodeComponent,
FiscalIdComponent,
PartnerTokenComponent,
GuildIdComponent,
FormFooterActionsComponent,
@@ -42,7 +42,7 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractFormDialog<
return this.fb.group({
name: fieldControl.name(this.initialValues?.name || ''),
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''),
fiscal_code: fieldControl.fiscal_code(this.initialValues?.fiscal_code || ''),
fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''),
partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''),
// @ts-ignore
guild_id: fieldControl.guild_id(this.initialValues?.guild?.id || ''),
@@ -3,7 +3,7 @@ import ISummary from '@/core/models/summary';
export interface IBusinessActivityRawResponse {
name: string;
economic_code: string;
fiscal_code: string;
fiscal_id: string;
partner_token: string;
guild: ISummary;
id: string;
@@ -13,7 +13,7 @@ export interface IBusinessActivityResponse extends IBusinessActivityRawResponse
export interface IBusinessActivityRequest {
name: string;
economic_code: string;
fiscal_code: string;
fiscal_id: string;
partner_token: string;
guild_id: string;
}
@@ -6,16 +6,27 @@
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-shared-upload-file accept="image/*" name="image" (onSelect)="changeFile($event)" />
@if (visible) {
<app-shared-upload-file
accept="image/*"
name="image"
[initial_file_url]="initialValues?.image_url || ''"
(onSelect)="changeFile($event)"
/>
}
<app-input label="عنوان" [control]="form.controls.name" name="name" />
<app-input label="شناسه عمومی" [control]="form.controls.sku" name="sku" />
<field-sku label="شناسه‌ی کالا" [control]="form.controls.sku_id" name="sku_id" />
<admin-guild-categories-select
[guildId]="guildId"
label="دسته‌بندی"
[control]="form.controls.category_id"
name="category_id"
/>
<app-enum-select type="unitType" [control]="form.controls.unit_type" name="unit_type" />
<catalog-measure-units-select
label="واحد اندازه‌گیری"
[control]="form.controls.measure_unit_id"
name="measure_unit_id"
/>
<app-enum-select type="goodPricingModel" [control]="form.controls.pricing_model" name="pricing_model" />
<app-input label="توضیحات" [control]="form.controls.description" name="description" />
@@ -1,18 +1,19 @@
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components';
import { InputComponent, SkuComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, inject, Input, signal } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { Maybe } from '@/core';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { CatalogMeasureUnitsSelectComponent } from '@/shared/catalog/measureUnits';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { onSelectFileArgs } from '@/shared/components/uploadFile/model';
import { SharedUploadFileComponent } from '@/shared/components/uploadFile/upload-file.component';
import { buildFormData } from '@/utils';
import { IGuildGoodRequest, IGuildGoodsResponse } from '../../models';
import { GuildGoodsService } from '../../services/goods.service';
import { GuildCategoriesSelectComponent } from '../categories/select.component';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
@Component({
selector: 'admin-guild-good-form',
@@ -25,6 +26,8 @@ import { SharedDialogComponent } from '@/shared/components/dialog/dialog.compone
EnumSelectComponent,
GuildCategoriesSelectComponent,
SharedUploadFileComponent,
SkuComponent,
CatalogMeasureUnitsSelectComponent,
],
})
export class GuildGoodFormComponent extends AbstractFormDialog<
@@ -40,8 +43,8 @@ export class GuildGoodFormComponent extends AbstractFormDialog<
form = this.fb.group({
name: [this.initialValues?.name || '', [Validators.required]],
sku: [this.initialValues?.sku || '', [Validators.required]],
unit_type: [this.initialValues?.unit_type || '', [Validators.required]],
sku_id: [this.initialValues?.sku.id || '', [Validators.required]],
measure_unit_id: [this.initialValues?.measure_unit.id || '', [Validators.required]],
pricing_model: [this.initialValues?.pricing_model || '', [Validators.required]],
category_id: [this.initialValues?.category.id || '', [Validators.required]],
description: [this.initialValues?.description || ''],
@@ -58,6 +61,8 @@ export class GuildGoodFormComponent extends AbstractFormDialog<
override ngOnChanges(): void {
this.form.patchValue(this.initialValues || {});
this.form.controls.category_id.setValue(this.initialValues?.category.id || '');
this.form.controls.measure_unit_id.setValue(this.initialValues?.measure_unit.id || '');
this.form.controls.sku_id.setValue(this.initialValues?.sku.id || '');
}
override submitForm(payload: IGuildGoodRequest) {
@@ -20,18 +20,13 @@ export class GuildGoodsListComponent extends AbstractList<IGuildGoodsResponse> {
@Input() header: IColumn[] = [
{ field: 'image_url', header: 'تصویر', type: 'thumbnail' },
{ field: 'name', header: 'عنوان' },
{ field: 'sku', header: 'شناسه‌ی عمومی' },
{ field: 'sku', header: 'شناسه‌ی کالا', type: 'nested', nestedOption: { path: 'sku.name' } },
{
field: 'category',
header: 'دسته‌بندی',
type: 'nested',
nestedOption: { path: 'category.name' },
},
{
field: 'created_at',
header: 'تاریخ ایجاد',
type: 'date',
},
];
service = inject(GuildGoodsService);
@@ -3,9 +3,10 @@ import ISummary from '@/core/models';
export interface IGuildGoodsRawResponse {
id: string;
name: string;
sku: string;
image_url: string;
sku: ISummary;
category: ISummary;
unit_type: string;
measure_unit: ISummary;
pricing_model: string;
description?: string;
created_at: string;
@@ -15,9 +16,9 @@ export interface IGuildGoodsResponse extends IGuildGoodsRawResponse {}
export interface IGuildGoodRequest {
name: string;
sku: string;
sku_id: string;
category_id: string;
unit_type: string;
measure_unit_id: string;
pricing_model: string;
description?: string;
}
@@ -0,0 +1,10 @@
<shared-dialog header="فرم واحد شمارش کالا" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<field-code [control]="form.controls.code" />
<field-name [control]="form.controls.name" />
<field-vat [control]="$any(form.controls.VAT)" />
<field-sku-type [control]="form.controls.type" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form>
</shared-dialog>
@@ -0,0 +1,67 @@
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { fieldControl } from '@/shared/constants/fields';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { CodeComponent, NameComponent, SkuTypeComponent, VatComponent } from '@/shared/components/fields';
import { Component, Input, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { IStockKeepingUnitRequest, IStockKeepingUnitResponse } from '../models';
import { StockKeepingUnitsService } from '../services/main.service';
@Component({
selector: 'stock-keeping-unit-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
SharedDialogComponent,
NameComponent,
CodeComponent,
VatComponent,
SkuTypeComponent,
FormFooterActionsComponent,
],
})
export class StockKeepingUnitFormComponent extends AbstractFormDialog<
IStockKeepingUnitRequest,
IStockKeepingUnitResponse
> {
@Input() stockKeepingUnitId?: string;
private readonly service = inject(StockKeepingUnitsService);
form = this.fb.group({
code: fieldControl.code(this.initialValues?.code || ''),
name: fieldControl.name(this.initialValues?.name || ''),
VAT: fieldControl.VAT(String(this.initialValues?.VAT ?? 0)),
type: fieldControl.type(this.initialValues?.type || 'GOLD'),
is_public: fieldControl.is_public(String(this.initialValues?.is_public ?? true), false),
is_domestic: fieldControl.is_domestic(String(this.initialValues?.is_domestic ?? true), false),
});
override ngOnChanges() {
this.form.patchValue({
code: this.initialValues?.code ?? '',
name: this.initialValues?.name ?? '',
VAT: String(this.initialValues?.VAT ?? 0),
type: this.initialValues?.type ?? 'GOLD',
is_public: String(this.initialValues?.is_public ?? true),
is_domestic: String(this.initialValues?.is_domestic ?? true),
});
if (this.editMode && !this.stockKeepingUnitId) {
throw 'missing some arguments';
}
}
override submitForm(payload: IStockKeepingUnitRequest) {
const preparedPayload: IStockKeepingUnitRequest = {
...payload,
VAT: Number(payload.VAT),
is_public: String(payload.is_public) === 'true',
is_domestic: String(payload.is_domestic) === 'true',
};
if (this.editMode) {
return this.service.update(this.stockKeepingUnitId!, preparedPayload);
}
return this.service.create(preparedPayload);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/admin/stock-keeping-units';
export const STOCK_KEEPING_UNITS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (id: string) => `${baseUrl}/${id}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes/index';
export * from './routes/index';
@@ -0,0 +1,18 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TStockKeepingUnitsRouteNames = 'stockKeepingUnits';
export const stockKeepingUnitsNamedRoutes: NamedRoutes<TStockKeepingUnitsRouteNames> = {
stockKeepingUnits: {
path: 'stock-keeping-units',
loadComponent: () =>
import('../../views/list.component').then((m) => m.StockKeepingUnitsComponent),
meta: {
title: 'واحدهای شمارش کالا',
pagePath: () => '/super_admin/stock-keeping-units',
},
},
};
export const STOCK_KEEPING_UNITS_ROUTES: Routes = Object.values(stockKeepingUnitsNamedRoutes);
@@ -0,0 +1 @@
export * from './io';
@@ -0,0 +1,21 @@
export interface IStockKeepingUnitRawResponse {
id: string;
code: string;
name: string;
VAT: number;
type: string;
is_public: boolean;
is_domestic: boolean;
created_at?: string;
}
export interface IStockKeepingUnitResponse extends IStockKeepingUnitRawResponse {}
export interface IStockKeepingUnitRequest {
code: string;
name: string;
VAT: number;
type?: string;
is_public?: boolean;
is_domestic?: boolean;
}
@@ -0,0 +1,29 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { STOCK_KEEPING_UNITS_API_ROUTES } from '../constants';
import {
IStockKeepingUnitRawResponse,
IStockKeepingUnitRequest,
IStockKeepingUnitResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class StockKeepingUnitsService {
constructor(private http: HttpClient) {}
private apiRoutes = STOCK_KEEPING_UNITS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IStockKeepingUnitResponse>> {
return this.http.get<IPaginatedResponse<IStockKeepingUnitRawResponse>>(this.apiRoutes.list());
}
create(payload: IStockKeepingUnitRequest): Observable<IStockKeepingUnitResponse> {
return this.http.post<IStockKeepingUnitResponse>(this.apiRoutes.list(), payload);
}
update(id: string, payload: IStockKeepingUnitRequest): Observable<IStockKeepingUnitResponse> {
return this.http.patch<IStockKeepingUnitResponse>(this.apiRoutes.single(id), payload);
}
}
@@ -0,0 +1 @@
export * from './list.component';
@@ -0,0 +1,24 @@
<app-page-data-list
[pageTitle]="'مدیریت شناسه‌ی کالاها'"
[addNewCtaLabel]="'افزودن شناسه‌ی کالا'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="شناسه‌ی کالایی یافت نشد"
emptyPlaceholderDescription="برای افزودن شناسه‌ی کالا، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="false"
[showAdd]="true"
(onAdd)="openAddForm()"
(onRefresh)="refresh()"
>
@if (selectedItemForEdit()) {
<stock-keeping-unit-form
[(visible)]="visibleForm"
[initialValues]="selectedItemForEdit()!"
[editMode]="editMode()"
[stockKeepingUnitId]="selectedItemForEdit()?.id"
(onSubmit)="refresh()"
/>
}
</app-page-data-list>
@@ -0,0 +1,28 @@
import { AbstractList } from '@/shared/abstractClasses/abstract-list';
import { PageDataListComponent } from '@/shared/components/pageDataList/page-data-list.component';
import { Component, inject } from '@angular/core';
import { StockKeepingUnitFormComponent } from '../components/form.component';
import { IStockKeepingUnitResponse } from '../models';
import { StockKeepingUnitsService } from '../services/main.service';
@Component({
selector: 'superAdmin-stockKeepingUnits',
templateUrl: './list.component.html',
imports: [PageDataListComponent, StockKeepingUnitFormComponent],
})
export class StockKeepingUnitsComponent extends AbstractList<IStockKeepingUnitResponse> {
private readonly service = inject(StockKeepingUnitsService);
override setColumns(): void {
this.columns = [
{ field: 'code', header: 'کد' },
{ field: 'name', header: 'عنوان' },
{ field: 'VAT', header: 'مالیات ارزش افزوده' },
{ field: 'type', header: 'نوع' },
];
}
override getDataRequest() {
return this.service.getAll();
}
}
+2
View File
@@ -6,6 +6,7 @@ import { GUILDS_ROUTES } from './modules/guilds/constants';
import { LICENSES_ROUTES } from './modules/licenses/constants';
import { SUPER_ADMIN_PARTNERS_ROUTES } from './modules/partners/constants';
import { PROVIDERS_ROUTES } from './modules/providers/constants';
import { STOCK_KEEPING_UNITS_ROUTES } from './modules/stockKeepingUnits/constants';
import { USERS_ROUTES } from './modules/users/constants';
export const SUPER_ADMIN_ROUTES = {
@@ -21,6 +22,7 @@ export const SUPER_ADMIN_ROUTES = {
...GUILDS_ROUTES,
...SUPER_ADMIN_PARTNERS_ROUTES,
...PROVIDERS_ROUTES,
...STOCK_KEEPING_UNITS_ROUTES,
...LICENSES_ROUTES,
...DEVICE_BRANDS_ROUTES,
...DEVICES_ROUTES,