feat: implement pos goods management module; add create, update, and list components with API integration

This commit is contained in:
2026-05-20 20:22:38 +03:30
parent c5e1fab09b
commit 1b4ac0789c
28 changed files with 437 additions and 65 deletions
+8 -4
View File
@@ -5,28 +5,28 @@
"endOfLine": "lf", "endOfLine": "lf",
"overrides": [ "overrides": [
{ {
"files": "*.html", "files": "**/*.html",
"options": { "options": {
"bracketSameLine": true, "bracketSameLine": true,
"printWidth": 120 "printWidth": 120
} }
}, },
{ {
"files": "*.scss", "files": "**/*.scss",
"options": { "options": {
"printWidth": 120, "printWidth": 120,
"singleQuote": false "singleQuote": false
} }
}, },
{ {
"files": "*.less", "files": "**/*.less",
"options": { "options": {
"printWidth": 120, "printWidth": 120,
"singleQuote": false "singleQuote": false
} }
}, },
{ {
"files": "*.json", "files": "**/*.json",
"options": { "options": {
"printWidth": 100 "printWidth": 100
} }
@@ -40,6 +40,10 @@
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 2,
"tailwindAttributes": [
"class",
"ngClass"
],
"trailingComma": "es5", "trailingComma": "es5",
"useTabs": false "useTabs": false
} }
-10
View File
@@ -55,16 +55,6 @@
"typescript": "~5.8.3" "typescript": "~5.8.3"
}, },
"name": "psp_panel", "name": "psp_panel",
"prettier": {
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ng build", "build": "ng build",
@@ -1,4 +1,4 @@
<shared-good-form <shared-good-form-dialog
[guildId]="guildId" [guildId]="guildId"
[visible]="visible" [visible]="visible"
(visibleChange)="visibleChange.emit($event)" (visibleChange)="visibleChange.emit($event)"
@@ -7,5 +7,4 @@
[createFn]="create" [createFn]="create"
[updateFn]="update" [updateFn]="update"
(onSubmit)="onSubmit.emit($event)" (onSubmit)="onSubmit.emit($event)"
(onClose)="onClose.emit()" (onClose)="onClose.emit()" />
/>
@@ -1,14 +1,15 @@
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog'; import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { IGoodResponse, SharedGoodFormComponent } from '@/shared/components/good'; import { IGoodResponse } from '@/shared/components/good';
import { ConsumerUserFormDialogComponent } from '@/shared/components/good/form-dialog.component';
import { IConsumerBusinessActivityGoodResponse } from '../../models/goods_io'; import { IConsumerBusinessActivityGoodResponse } from '../../models/goods_io';
import { BusinessActivityGoodsService } from '../../services/goods.service'; import { BusinessActivityGoodsService } from '../../services/goods.service';
@Component({ @Component({
selector: 'consumer-businessActivity-good-form', selector: 'consumer-businessActivity-good-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
imports: [SharedGoodFormComponent], imports: [ConsumerUserFormDialogComponent],
}) })
export class ConsumerBusinessActivityGoodFormComponent extends AbstractDialog { export class ConsumerBusinessActivityGoodFormComponent extends AbstractDialog {
@Input({ required: true }) businessId!: string; @Input({ required: true }) businessId!: string;
@@ -8,6 +8,7 @@ import { Ripple } from 'primeng/ripple';
import config from 'src/config'; import config from 'src/config';
import { posAboutNamedRoutes } from '../../modules/about/constants'; import { posAboutNamedRoutes } from '../../modules/about/constants';
import { posConfigNamedRoutes } from '../../modules/configs/constants'; import { posConfigNamedRoutes } from '../../modules/configs/constants';
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/constants'; import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/constants';
import { posSupportNamedRoutes } from '../../modules/support/constants'; import { posSupportNamedRoutes } from '../../modules/support/constants';
import { PosInfoStore } from '../../store'; import { PosInfoStore } from '../../store';
@@ -45,6 +46,11 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(), routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
icon: 'pi pi-fw pi-receipt', icon: 'pi pi-fw pi-receipt',
}, },
{
label: 'کالاها',
routerLink: PosGoodsManagementRoutes.goods.meta.pagePath!(),
icon: 'pi pi-fw pi-objects-column',
},
{ {
label: 'تنظیمات', label: 'تنظیمات',
routerLink: posConfigNamedRoutes.config.meta.pagePath!(), routerLink: posConfigNamedRoutes.config.meta.pagePath!(),
@@ -0,0 +1 @@
export * from './list.component';
@@ -0,0 +1,46 @@
<div class="bg-surface-card">
<app-inner-pages-header [pageTitle]="preparedPageTitle()" [backRoute]="backRoute">
<ng-template #actions>
<button pButton icon="pi pi-refresh" type="button" size="small" outlined (click)="refresh()"></button>
</ng-template>
</app-inner-pages-header>
</div>
<hr class="mt-0!" />
<div class="flex w-full flex-col flex-wrap gap-3 px-4 py-2">
@if (loading()) {
@for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) {
<div class="h-14 w-full">
<p-skeleton width="100%" height="100%"></p-skeleton>
</div>
}
} @else {
@for (good of items(); track good.id) {
<div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1 shadow-xs">
<div class="flex grow items-center gap-2">
<img
[src]="good.image_url || goodPlaceholder"
loading="lazy"
decoding="async"
class="h-12 w-12 rounded-md object-cover" />
<div class="flex flex-col gap-2">
<span class="text-base font-bold">
{{ good.name }}
</span>
<span class="text-muted-color text-sm">
{{ good.sku.code }}
</span>
</div>
</div>
<div class="flex shrink-0 items-center justify-between gap-1">
<p-button
type="button"
label="ویرایش"
icon="pi pi-pen-to-square"
outlined
size="small"
[routerLink]="editPageRoute(good.id)"></p-button>
</div>
</div>
}
}
</div>
@@ -0,0 +1,56 @@
import { POS_ROUTES } from '@/domains/pos/routes';
import { PosInfoStore } from '@/domains/pos/store';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { Component, computed, inject, signal } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton';
import { finalize } from 'rxjs';
import images from 'src/assets/images';
import { PosGoodsManagementRoutes } from '../constants';
import { IPosGoodsManagementResponse } from '../models';
import { PosOwnedGoodsService } from '../services/main.service';
@Component({
selector: 'pos-ownedGoods-list',
templateUrl: './list.component.html',
imports: [InnerPagesHeaderComponent, ButtonDirective, Skeleton, Button, RouterLink],
})
export class PosOwnedGoodsListComponent {
private readonly service = inject(PosOwnedGoodsService);
private readonly infoStore = inject(PosInfoStore);
private readonly router = inject(Router);
goodPlaceholder = images.placeholders.default;
loading = signal(true);
items = signal<IPosGoodsManagementResponse[]>([]);
backRoute = POS_ROUTES.path || '/';
preparedPageTitle = computed(
() => `لیست کالاهای ${this.infoStore.entity()?.businessActivity.name}`
);
ngOnInit() {
this.getData();
}
getData() {
this.loading.set(true);
this.service
.getAll()
.pipe(finalize(() => this.loading.set(false)))
.subscribe((res) => {
this.items.set(res.data ?? []);
});
}
refresh() {
this.getData();
}
editPageRoute(goodId: string) {
return PosGoodsManagementRoutes.update.meta.pagePath!(goodId);
}
}
@@ -0,0 +1,8 @@
const baseUrl = () => `/api/v1/pos/owned-goods`;
export const POS_OWNED_GOODS_API_ROUTES = {
getAll: () => `${baseUrl()}`,
get: (good_id: string) => `${baseUrl()}/${good_id}`,
create: () => `${baseUrl()}`,
update: (good_id: string) => `${baseUrl()}/${good_id}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes/index';
export * from './routes/index';
@@ -0,0 +1,43 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
import config from 'src/config';
export type TPosGoodsManagementRouteNames = 'goods' | 'update' | 'create';
const baseRoute = `${config.isPosApplication ? '' : '/pos'}/goods`;
export const PosGoodsManagementRoutes: NamedRoutes<TPosGoodsManagementRouteNames> = {
goods: {
path: 'goods',
loadComponent: () =>
import('../../views/list.component').then((m) => m.PosGoodsManagementListComponent),
meta: {
title: 'مدیریت کالاها',
pagePath: () => baseRoute,
},
},
update: {
path: 'goods/:goodId',
loadComponent: () =>
import('../../views/update.component').then((m) => m.PosGoodsUpdateComponent),
meta: {
title: 'ویرایش کالا',
pagePath: (goodId: string) => `${baseRoute}/${goodId}`,
},
},
create: {
path: 'goods/create',
loadComponent: () =>
import('../../views/create.component').then((m) => m.PosGoodsCreateComponent),
meta: {
title: 'ایجاد کالا',
pagePath: () => baseRoute,
},
},
};
export const POS_GOODS_MANAGEMENT_ROUTES: Routes = [
PosGoodsManagementRoutes.goods,
PosGoodsManagementRoutes.create,
PosGoodsManagementRoutes.update,
];
@@ -0,0 +1 @@
export * from './io';
@@ -0,0 +1,7 @@
import { IGoodResponse } from '@/domains/pos/models';
import { IGoodRequestPayload } from '@/shared/components/good';
export interface IPosGoodsManagementRawResponse extends IGoodResponse {}
export interface IPosGoodsManagementResponse extends IPosGoodsManagementRawResponse {}
export interface IPosGoodsManagementRequestPayload extends IGoodRequestPayload {}
@@ -0,0 +1,29 @@
import { IListingResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { POS_OWNED_GOODS_API_ROUTES } from '../constants';
import { IPosGoodsManagementRawResponse, IPosGoodsManagementResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class PosOwnedGoodsService {
constructor(private http: HttpClient) {}
private apiRoutes = POS_OWNED_GOODS_API_ROUTES;
getAll(): Observable<IListingResponse<IPosGoodsManagementResponse>> {
return this.http.get<IListingResponse<IPosGoodsManagementRawResponse>>(this.apiRoutes.getAll());
}
get(goodId: string): Observable<IPosGoodsManagementResponse> {
return this.http.get<IPosGoodsManagementRawResponse>(this.apiRoutes.get(goodId));
}
create(data: FormData): Observable<IPosGoodsManagementResponse> {
return this.http.post<IPosGoodsManagementRawResponse>(this.apiRoutes.create(), data);
}
update(goodId: string, payload: FormData): Observable<IPosGoodsManagementResponse> {
return this.http.patch<IPosGoodsManagementRawResponse>(this.apiRoutes.update(goodId), payload);
}
}
@@ -0,0 +1,7 @@
<div class="bg-surface-card">
<app-inner-pages-header [pageTitle]="preparedPageTitle()" [backRoute]="backRoute" />
<hr class="mt-0!" />
<div class="flex w-full flex-col flex-wrap gap-3 p-4">
<shared-good-form [guildId]="guildId()" [createFn]="createFn" (onSubmit)="onSubmit()" (onClose)="onPrev()" />
</div>
</div>
@@ -0,0 +1,36 @@
import { PosInfoStore } from '@/domains/pos/store';
import { SharedGoodFormComponent } from '@/shared/components/good';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { Component, computed, inject } from '@angular/core';
import { Router } from '@angular/router';
import config from 'src/config';
import { PosOwnedGoodsService } from '../services/main.service';
@Component({
selector: 'pos-good-create',
templateUrl: './create.component.html',
imports: [SharedGoodFormComponent, InnerPagesHeaderComponent],
})
export class PosGoodsCreateComponent {
private readonly router = inject(Router);
private readonly store = inject(PosInfoStore);
private readonly service = inject(PosOwnedGoodsService);
readonly guildId = computed(() => this.store.entity()?.guild.id || '');
readonly backRoute = config.isPosApplication ? '/' : '/pos';
preparedPageTitle = computed(
() => `ایجاد کالا برای ${this.store.entity()?.businessActivity.name}`
);
onSubmit() {
this.onPrev();
}
onPrev() {
this.router.navigateByUrl(this.backRoute);
}
createFn = (payload: FormData) => this.service.create(payload);
}
@@ -0,0 +1,3 @@
export * from './create.component';
export * from './list.component';
export * from './update.component';
@@ -0,0 +1 @@
<pos-ownedGoods-list />
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { PosOwnedGoodsListComponent } from '../components';
@Component({
selector: 'pos-goodsManagement-list',
templateUrl: './list.component.html',
imports: [PosOwnedGoodsListComponent],
})
export class PosGoodsManagementListComponent {}
@@ -0,0 +1,16 @@
<div class="bg-surface-card">
<app-inner-pages-header [pageTitle]="preparedPageTitle()" [backRoute]="backRoute" />
<hr class="mt-0!" />
<div class="flex w-full flex-col flex-wrap gap-3 p-4">
@if (loading()) {
} @else if (item()) {
<shared-good-form
[guildId]="guildId()"
[initialValues]="item()!"
[updateFn]="updateFn"
[editMode]="true"
(onSubmit)="onSubmit()"
(onClose)="onPrev()" />
}
</div>
</div>
@@ -0,0 +1,60 @@
import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store';
import { SharedGoodFormComponent } from '@/shared/components/good';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import pageParamsUtils from '@/utils/page-params.utils';
import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { finalize } from 'rxjs';
import { PosGoodsManagementRoutes } from '../constants';
import { IPosGoodsManagementResponse } from '../models';
import { PosOwnedGoodsService } from '../services/main.service';
@Component({
selector: 'pos-good-update',
templateUrl: './update.component.html',
imports: [SharedGoodFormComponent, InnerPagesHeaderComponent],
})
export class PosGoodsUpdateComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly store = inject(PosInfoStore);
private readonly service = inject(PosOwnedGoodsService);
loading = signal(true);
item = signal<Maybe<IPosGoodsManagementResponse>>(null);
pageParams = computed(() => pageParamsUtils(this.route));
goodId = signal<string>(this.pageParams()['goodId']);
readonly backRoute = PosGoodsManagementRoutes.goods.meta.pagePath!();
preparedPageTitle = computed(() => `ویرایش ${this.item()?.name || ''}`);
readonly guildId = computed(() => this.store.entity()?.guild.id || '');
ngOnInit() {
this.loading.set(true);
this.service
.get(this.goodId())
.pipe(
finalize(() => {
this.loading.set(false);
})
)
.subscribe((res) => {
this.item.set(res);
});
}
onSubmit() {
this.onPrev();
}
onPrev() {
this.router.navigateByUrl(PosGoodsManagementRoutes.goods.meta.pagePath!());
}
updateFn = (payload: FormData) => {
return this.service.update(this.goodId(), payload);
};
}
@@ -1,33 +1,32 @@
<div class="flex flex-col gap-3 flex-wrap"> <div class="flex flex-col flex-wrap gap-3">
@if (loading()) { @if (loading()) {
@for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) { @for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) {
<div class="w-100 h-12"> <div class="h-12 w-100">
<p-skeleton width="100%" height="100%"></p-skeleton> <p-skeleton width="100%" height="100%"></p-skeleton>
</div> </div>
} }
} @else { } @else {
@for (good of visibleGoods(); track good.id) { @for (good of visibleGoods(); track good.id) {
<div class="flex items-center bg-surface-card rounded-lg py-1 px-3 shadow-xs gap-2"> <div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1 shadow-xs">
<div class="grow flex items-center gap-2"> <div class="flex grow items-center gap-2">
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
class="w-12 h-12 object-cover rounded-md" class="h-12 w-12 rounded-md object-cover" />
/>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-base font-bold"> <span class="text-base font-bold">
{{ good.name }} {{ good.name }}
@if (!good.is_default_guild_good) { @if (!good.is_default_guild_good) {
<small class="text-xs text-muted-color">*</small> <small class="text-muted-color text-xs">*</small>
} }
</span> </span>
<span class="text-sm text-muted-color"> <span class="text-muted-color text-sm">
{{ good.sku.code }} {{ good.sku.code }}
</span> </span>
</div> </div>
</div> </div>
<div class="shrink-0 flex items-center justify-between gap-1"> <div class="flex shrink-0 items-center justify-between gap-1">
<pos-good-favorite-cta [isFavorite]="good.is_favorite" [id]="good.id" size="small" /> <pos-good-favorite-cta [isFavorite]="good.is_favorite" [id]="good.id" size="small" />
<button pButton type="button" label="انتخاب" class="w-28" size="small" (click)="addGood(good)"></button> <button pButton type="button" label="انتخاب" class="w-28" size="small" (click)="addGood(good)"></button>
</div> </div>
@@ -61,7 +61,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
info: {} as TerminalSuccessPaymentPayload, info: {} as TerminalSuccessPaymentPayload,
label: `${i + 1} مرحله‌ای`, label: `${i + 1} مرحله‌ای`,
amount: 0, amount: 0,
})), }))
); );
selectedPayByTerminalStep = signal(1); selectedPayByTerminalStep = signal(1);
@@ -89,14 +89,14 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}; };
} }
return step; return step;
}), })
); );
const terminalControl = this.terminalControls[paidStepId - 1]; const terminalControl = this.terminalControls[paidStepId - 1];
if (terminalControl) { if (terminalControl) {
terminalControl.disable({ onlySelf: true, emitEvent: false }); terminalControl.disable({ onlySelf: true, emitEvent: false });
} }
const amount = Number( const amount = Number(
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0, terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0
); );
this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` }); this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` });
} else { } else {
@@ -106,7 +106,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
ngAfterViewInit() { ngAfterViewInit() {
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener( this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener(
this.injectedPaymentResultHandler, this.injectedPaymentResultHandler
); );
} }
@@ -235,19 +235,19 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
this.form.controls.terminals this.form.controls.terminals
.at(terminalIndex) .at(terminalIndex)
?.setValue( ?.setValue(
(this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount(), (this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount()
); );
break; break;
case 'CASH': case 'CASH':
this.form.controls.cash.setValue( this.form.controls.cash.setValue(
(this.form.controls.cash.value || 0) + this.remainedAmount(), (this.form.controls.cash.value || 0) + this.remainedAmount()
); );
break; break;
case 'SET_OFF': case 'SET_OFF':
this.form.controls.set_off.setValue( this.form.controls.set_off.setValue(
(this.form.controls.set_off.value || 0) + this.remainedAmount(), (this.form.controls.set_off.value || 0) + this.remainedAmount()
); );
break; break;
} }
@@ -300,7 +300,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
.pipe( .pipe(
catchError((err) => { catchError((err) => {
return throwError(() => err); return throwError(() => err);
}), })
) )
.subscribe((res) => { .subscribe((res) => {
this.close(); this.close();
+2
View File
@@ -1,6 +1,7 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { POS_ABOUT_ROUTES } from './modules/about/constants'; import { POS_ABOUT_ROUTES } from './modules/about/constants';
import { POS_CONFIG_ROUTES } from './modules/configs/constants'; import { POS_CONFIG_ROUTES } from './modules/configs/constants';
import { POS_GOODS_MANAGEMENT_ROUTES } from './modules/goodsManagement/constants';
import { POS_SALE_INVOICES_ROUTES } from './modules/saleInvoices/constants'; import { POS_SALE_INVOICES_ROUTES } from './modules/saleInvoices/constants';
import { POS_SUPPORT_ROUTES } from './modules/support/constants'; import { POS_SUPPORT_ROUTES } from './modules/support/constants';
@@ -17,5 +18,6 @@ export const POS_ROUTES = {
...POS_ABOUT_ROUTES, ...POS_ABOUT_ROUTES,
...POS_SUPPORT_ROUTES, ...POS_SUPPORT_ROUTES,
...POS_CONFIG_ROUTES, ...POS_CONFIG_ROUTES,
...POS_GOODS_MANAGEMENT_ROUTES,
], ],
} as Route; } as Route;
@@ -0,0 +1,18 @@
<shared-dialog
[header]="preparedTitle"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '500px' }"
[closable]="true"
(onHide)="close()">
@if (visible) {
<shared-good-form
[guildId]="guildId"
[initialValues]="initialValues"
[editMode]="editMode"
[createFn]="createFn"
[updateFn]="updateFn"
(onSubmit)="onFormSubmit($event)"
(onClose)="close()" />
}
</shared-dialog>
@@ -0,0 +1,30 @@
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { SharedGoodFormComponent } from './form.component';
import { IGoodResponse } from './types';
@Component({
selector: 'shared-good-form-dialog',
templateUrl: './form-dialog.component.html',
imports: [SharedDialogComponent, SharedGoodFormComponent],
})
export class ConsumerUserFormDialogComponent extends AbstractDialog {
@Output() onSubmit = new EventEmitter<IGoodResponse>();
@Input() initialValues?: IGoodResponse;
@Input() editMode?: boolean;
@Input({ required: true }) guildId!: string;
@Input() createFn?: (payload: FormData) => Observable<IGoodResponse>;
@Input() updateFn?: (payload: FormData) => Observable<IGoodResponse>;
get preparedTitle() {
return `${this.editMode ? 'ویرایش' : 'افزودن'} کالا`;
}
onFormSubmit(response: IGoodResponse) {
this.onSubmit.emit(response);
this.close();
}
}
@@ -1,19 +1,22 @@
<shared-dialog [header]="preparedTitle" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
[closable]="true"> <app-shared-upload-file
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> accept="image/*"
@if (visible) { name="image"
<app-shared-upload-file accept="image/*" name="image" [initial_file_url]="initialValues?.image_url || ''" [initial_file_url]="initialValues?.image_url || ''"
(onSelect)="changeFile($event)" /> (onSelect)="changeFile($event)" />
} <field-name [control]="form.controls.name" name="name" />
<field-name [control]="form.controls.name" name="name" /> <field-sku label="شناسه کالا" [control]="form.controls.sku_id" name="sku_id" />
<field-sku label="شناسه کالا" [control]="form.controls.sku_id" name="sku_id" /> <catalog-guild-goodCategories-select
<catalog-guild-goodCategories-select [guildId]="guildId" label="دسته‌بندی" [control]="form.controls.category_id" [guildId]="guildId"
name="category_id" /> label="دسته‌بندی"
<catalog-measure-units-select label="واحد اندازه‌گیری" [control]="form.controls.measure_unit_id" [control]="form.controls.category_id"
name="measure_unit_id" /> name="category_id" />
<app-enum-select type="goodPricingModel" [control]="form.controls.pricing_model" name="pricing_model" /> <catalog-measure-units-select
<field-description [control]="form.controls.description" /> label="واحد اندازه‌گیری"
[control]="form.controls.measure_unit_id"
name="measure_unit_id" />
<app-enum-select type="goodPricingModel" [control]="form.controls.pricing_model" name="pricing_model" />
<field-description [control]="form.controls.description" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
</shared-dialog>
@@ -1,4 +1,4 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { DescriptionComponent, NameComponent, SkuComponent } from '@/shared/components'; import { DescriptionComponent, NameComponent, SkuComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, Input, signal } from '@angular/core'; import { Component, Input, signal } from '@angular/core';
@@ -8,7 +8,6 @@ import { Maybe } from '@/core';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component'; import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { CatalogGuildGoodCategoriesSelectComponent } from '@/shared/catalog/guildGoodCategories'; import { CatalogGuildGoodCategoriesSelectComponent } from '@/shared/catalog/guildGoodCategories';
import { CatalogMeasureUnitsSelectComponent } from '@/shared/catalog/measureUnits'; import { CatalogMeasureUnitsSelectComponent } from '@/shared/catalog/measureUnits';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { onSelectFileArgs } from '@/shared/components/uploadFile/model'; import { onSelectFileArgs } from '@/shared/components/uploadFile/model';
import { fieldControl } from '@/shared/constants'; import { fieldControl } from '@/shared/constants';
import { buildFormData } from '@/utils'; import { buildFormData } from '@/utils';
@@ -21,7 +20,6 @@ import { IGoodRequestPayload, IGoodResponse } from './types';
templateUrl: './form.component.html', templateUrl: './form.component.html',
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
SharedDialogComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
EnumSelectComponent, EnumSelectComponent,
SkuComponent, SkuComponent,
@@ -32,13 +30,10 @@ import { IGoodRequestPayload, IGoodResponse } from './types';
CatalogGuildGoodCategoriesSelectComponent, CatalogGuildGoodCategoriesSelectComponent,
], ],
}) })
export class SharedGoodFormComponent extends AbstractFormDialog< export class SharedGoodFormComponent extends AbstractForm<IGoodRequestPayload, IGoodResponse> {
IGoodRequestPayload,
IGoodResponse
> {
@Input({ required: true }) guildId!: string; @Input({ required: true }) guildId!: string;
@Input({ required: true }) createFn!: (payload: FormData) => Observable<IGoodResponse>; @Input() createFn?: (payload: FormData) => Observable<IGoodResponse>;
@Input({ required: true }) updateFn!: (payload: FormData) => Observable<IGoodResponse>; @Input() updateFn?: (payload: FormData) => Observable<IGoodResponse>;
goodImage = signal<Maybe<File>>(null); goodImage = signal<Maybe<File>>(null);
@@ -69,8 +64,8 @@ export class SharedGoodFormComponent extends AbstractFormDialog<
override submitForm() { override submitForm() {
const formData = buildFormData(this.form, { image: this.goodImage() }); const formData = buildFormData(this.form, { image: this.goodImage() });
if (this.editMode) { if (this.editMode) {
return this.updateFn(formData); return this.updateFn?.(formData);
} }
return this.createFn(formData); return this.createFn?.(formData);
} }
} }