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
@@ -1,4 +1,4 @@
<shared-good-form
<shared-good-form-dialog
[guildId]="guildId"
[visible]="visible"
(visibleChange)="visibleChange.emit($event)"
@@ -7,5 +7,4 @@
[createFn]="create"
[updateFn]="update"
(onSubmit)="onSubmit.emit($event)"
(onClose)="onClose.emit()"
/>
(onClose)="onClose.emit()" />
@@ -1,14 +1,15 @@
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
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 { BusinessActivityGoodsService } from '../../services/goods.service';
@Component({
selector: 'consumer-businessActivity-good-form',
templateUrl: './form.component.html',
imports: [SharedGoodFormComponent],
imports: [ConsumerUserFormDialogComponent],
})
export class ConsumerBusinessActivityGoodFormComponent extends AbstractDialog {
@Input({ required: true }) businessId!: string;
@@ -8,6 +8,7 @@ import { Ripple } from 'primeng/ripple';
import config from 'src/config';
import { posAboutNamedRoutes } from '../../modules/about/constants';
import { posConfigNamedRoutes } from '../../modules/configs/constants';
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/constants';
import { posSupportNamedRoutes } from '../../modules/support/constants';
import { PosInfoStore } from '../../store';
@@ -45,6 +46,11 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
icon: 'pi pi-fw pi-receipt',
},
{
label: 'کالاها',
routerLink: PosGoodsManagementRoutes.goods.meta.pagePath!(),
icon: 'pi pi-fw pi-objects-column',
},
{
label: 'تنظیمات',
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()) {
@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>
</div>
}
} @else {
@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="grow flex items-center gap-2">
<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="w-12 h-12 object-cover rounded-md"
/>
class="h-12 w-12 rounded-md object-cover" />
<div class="flex flex-col gap-2">
<span class="text-base font-bold">
{{ good.name }}
@if (!good.is_default_guild_good) {
<small class="text-xs text-muted-color">*</small>
<small class="text-muted-color text-xs">*</small>
}
</span>
<span class="text-sm text-muted-color">
<span class="text-muted-color text-sm">
{{ good.sku.code }}
</span>
</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" />
<button pButton type="button" label="انتخاب" class="w-28" size="small" (click)="addGood(good)"></button>
</div>
@@ -61,7 +61,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
info: {} as TerminalSuccessPaymentPayload,
label: `${i + 1} مرحله‌ای`,
amount: 0,
})),
}))
);
selectedPayByTerminalStep = signal(1);
@@ -89,14 +89,14 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
};
}
return step;
}),
})
);
const terminalControl = this.terminalControls[paidStepId - 1];
if (terminalControl) {
terminalControl.disable({ onlySelf: true, emitEvent: false });
}
const amount = Number(
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0,
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0
);
this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` });
} else {
@@ -106,7 +106,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
ngAfterViewInit() {
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener(
this.injectedPaymentResultHandler,
this.injectedPaymentResultHandler
);
}
@@ -235,19 +235,19 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
this.form.controls.terminals
.at(terminalIndex)
?.setValue(
(this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount(),
(this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount()
);
break;
case 'CASH':
this.form.controls.cash.setValue(
(this.form.controls.cash.value || 0) + this.remainedAmount(),
(this.form.controls.cash.value || 0) + this.remainedAmount()
);
break;
case 'SET_OFF':
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;
}
@@ -300,7 +300,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
.pipe(
catchError((err) => {
return throwError(() => err);
}),
})
)
.subscribe((res) => {
this.close();
+2
View File
@@ -1,6 +1,7 @@
import { Route } from '@angular/router';
import { POS_ABOUT_ROUTES } from './modules/about/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_SUPPORT_ROUTES } from './modules/support/constants';
@@ -17,5 +18,6 @@ export const POS_ROUTES = {
...POS_ABOUT_ROUTES,
...POS_SUPPORT_ROUTES,
...POS_CONFIG_ROUTES,
...POS_GOODS_MANAGEMENT_ROUTES,
],
} as Route;