feat: implement product creation and update features with form handling and routing
This commit is contained in:
+2
-1
@@ -33,7 +33,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"prettier-plugin-organize-imports"
|
"prettier-plugin-organize-imports",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
],
|
],
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { ProductBrandsSelectComponent } from '@/modules/productBrands/components
|
|||||||
import { ProductCategoriesSelectComponent } from '@/modules/productCategories/components';
|
import { ProductCategoriesSelectComponent } from '@/modules/productCategories/components';
|
||||||
import { InputComponent } from '@/shared/components';
|
import { InputComponent } from '@/shared/components';
|
||||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
import { UikitFieldComponent } from '@/uikit';
|
|
||||||
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Dialog } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
@@ -17,7 +16,6 @@ import { ProductsService } from '../services/main.service';
|
|||||||
Dialog,
|
Dialog,
|
||||||
InputComponent,
|
InputComponent,
|
||||||
FormFooterActionsComponent,
|
FormFooterActionsComponent,
|
||||||
UikitFieldComponent,
|
|
||||||
ProductBrandsSelectComponent,
|
ProductBrandsSelectComponent,
|
||||||
ProductCategoriesSelectComponent,
|
ProductCategoriesSelectComponent,
|
||||||
],
|
],
|
||||||
@@ -49,8 +47,8 @@ export class ProductFormComponent {
|
|||||||
name: [this.initialValues?.name || null, [Validators.required]],
|
name: [this.initialValues?.name || null, [Validators.required]],
|
||||||
description: [this.initialValues?.description || null],
|
description: [this.initialValues?.description || null],
|
||||||
// productType: [this.initialValues?.productType || null, [Validators.required]],
|
// productType: [this.initialValues?.productType || null, [Validators.required]],
|
||||||
brandId: [this.initialValues?.brandId || null, [Validators.required]],
|
brandId: [this.initialValues?.brand?.id || null, [Validators.required]],
|
||||||
categoryId: [this.initialValues?.categoryId || null],
|
categoryId: [this.initialValues?.category?.id || null],
|
||||||
});
|
});
|
||||||
|
|
||||||
submitLoading = signal(false);
|
submitLoading = signal(false);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center gap-5 justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" />
|
||||||
|
<h3 class="m-0">{{ initialData ? "ویرایش محصول" : "ایجاد محصول جدید" }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
label="انصراف"
|
||||||
|
type="button"
|
||||||
|
outlined
|
||||||
|
icon="pi pi-times"
|
||||||
|
routerLink="/products"
|
||||||
|
[disabled]="form.disabled || loading"
|
||||||
|
></button>
|
||||||
|
<button pButton label="ذخیره" type="submit" icon="pi pi-check" [loading]="loading" (click)="submit()"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="flex flex-col gap-3 grow">
|
||||||
|
<p-card header="جزئیات محصول" class="w-full">
|
||||||
|
<form [formGroup]="form" (submit)="submit()" class="grid grid-cols-2 gap-4">
|
||||||
|
<app-input label="نام" [control]="form.controls.name" name="name" class="col-span-2" />
|
||||||
|
<app-input label="SKU" [control]="form.controls.sku" name="sku" />
|
||||||
|
<barcode-input [control]="form.controls.barcode" name="barcode" />
|
||||||
|
<product-brands-select-field [control]="form.controls.brandId" [canInsert]="true" />
|
||||||
|
<uikit-field label="توضیحات" [control]="form.controls.description" name="description" class="col-span-2">
|
||||||
|
<textarea
|
||||||
|
pTextarea
|
||||||
|
[formControl]="form.controls.description"
|
||||||
|
name="description"
|
||||||
|
rows="4"
|
||||||
|
class="w-full"
|
||||||
|
></textarea>
|
||||||
|
</uikit-field>
|
||||||
|
</form>
|
||||||
|
</p-card>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 w-md">
|
||||||
|
<div class="flex flex-col gap-3 grow">
|
||||||
|
<p-card header="قیمت">
|
||||||
|
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||||
|
<app-input label="قیمت پایهی فروش" [control]="form.controls.basePrice" name="basePrice" />
|
||||||
|
</form>
|
||||||
|
</p-card>
|
||||||
|
<p-card header="اطلاعات تکمیلی">
|
||||||
|
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||||
|
<product-categories-select-field [control]="form.controls.categoryId" [canInsert]="true" />
|
||||||
|
</form>
|
||||||
|
</p-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { ProductBrandsSelectComponent } from '@/modules/productBrands/components';
|
||||||
|
import { ProductCategoriesSelectComponent } from '@/modules/productCategories/components';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { BarcodeInputComponent } from '@/shared/components/barcodeInput/barcode-input.component';
|
||||||
|
import { UikitFieldComponent } from '@/uikit';
|
||||||
|
import { Component, effect, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
|
import { IProductRequest, IProductResponse } from '../../models';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'product-full-form',
|
||||||
|
templateUrl: './full-form.component.html',
|
||||||
|
imports: [
|
||||||
|
Button,
|
||||||
|
RouterLink,
|
||||||
|
ButtonDirective,
|
||||||
|
Card,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputComponent,
|
||||||
|
UikitFieldComponent,
|
||||||
|
TextareaModule,
|
||||||
|
BarcodeInputComponent,
|
||||||
|
ProductBrandsSelectComponent,
|
||||||
|
ProductCategoriesSelectComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ProductFullFormComponent {
|
||||||
|
@Input() initialData?: IProductResponse;
|
||||||
|
@Input() loading: boolean = false;
|
||||||
|
@Output() onSubmit = new EventEmitter<IProductRequest>();
|
||||||
|
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if (this.initialData) {
|
||||||
|
this.form.patchValue({
|
||||||
|
...this.initialData,
|
||||||
|
brandId: this.initialData.brand?.id || null,
|
||||||
|
categoryId: this.initialData.category?.id || null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
name: [this.initialData?.name || '', [Validators.required]],
|
||||||
|
brandId: [this.initialData?.brand?.id || null, [Validators.required]],
|
||||||
|
sku: [this.initialData?.sku || null],
|
||||||
|
barcode: [this.initialData?.barcode || null],
|
||||||
|
description: [this.initialData?.description || null],
|
||||||
|
categoryId: [this.initialData?.category?.id || null],
|
||||||
|
imageUrl: [null],
|
||||||
|
basePrice: [this.initialData?.basePrice || 0, [Validators.required, Validators.min(0)]],
|
||||||
|
});
|
||||||
|
|
||||||
|
get backRoute() {
|
||||||
|
return this.initialData ? ['/products', this.initialData.id] : ['/products'];
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.form.markAsTouched();
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.onSubmit.emit(this.form.value as unknown as IProductRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NamedRoutes } from '@/core';
|
import { NamedRoutes } from '@/core';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export type TProductsRouteNames = 'products' | 'product';
|
export type TProductsRouteNames = 'products' | 'product' | 'create' | 'update';
|
||||||
|
|
||||||
export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
||||||
products: {
|
products: {
|
||||||
@@ -12,6 +12,15 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
|||||||
pagePath: () => '/products',
|
pagePath: () => '/products',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
create: {
|
||||||
|
path: 'products/create',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/create.component').then((m) => m.CreateProductComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'ایجاد محصول جدید',
|
||||||
|
pagePath: () => '/products/create',
|
||||||
|
},
|
||||||
|
},
|
||||||
product: {
|
product: {
|
||||||
path: 'products/:productId',
|
path: 'products/:productId',
|
||||||
loadComponent: () => import('../../views/single.component').then((m) => m.ProductComponent),
|
loadComponent: () => import('../../views/single.component').then((m) => m.ProductComponent),
|
||||||
@@ -20,6 +29,15 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
|||||||
pagePath: () => '/products/:productId',
|
pagePath: () => '/products/:productId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
path: 'products/:productId/update',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/update.component').then((m) => m.UpdateProductComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'محصول',
|
||||||
|
pagePath: () => '/products/:productId/update',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRODUCTS_ROUTES: Routes = Object.values(productsNamedRoutes);
|
export const PRODUCTS_ROUTES: Routes = Object.values(productsNamedRoutes);
|
||||||
|
|||||||
+18
-10
@@ -1,23 +1,31 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { IProductBrand, IProductCategory } from './others';
|
||||||
|
|
||||||
export interface IProductRawResponse {
|
export interface IProductRawResponse {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description?: string;
|
||||||
productType: string;
|
barcode?: Maybe<string>;
|
||||||
|
sku?: Maybe<string>;
|
||||||
|
// productType: string;
|
||||||
|
brand?: IProductBrand;
|
||||||
|
category?: IProductCategory;
|
||||||
|
// supplierId: number;
|
||||||
|
basePrice?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt: Date;
|
deletedAt?: Maybe<Date>;
|
||||||
brandId: number;
|
|
||||||
categoryId: number;
|
|
||||||
supplierId: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProductResponse extends IProductRawResponse {}
|
export interface IProductResponse extends IProductRawResponse {}
|
||||||
|
|
||||||
export interface IProductRequest {
|
export interface IProductRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
// productType: string;
|
||||||
productType: string;
|
|
||||||
brandId: number;
|
brandId: number;
|
||||||
categoryId: number;
|
description: Maybe<string>;
|
||||||
supplierId: number;
|
barcode: Maybe<string>;
|
||||||
|
sku: Maybe<string>;
|
||||||
|
categoryId: Maybe<number>;
|
||||||
|
// supplierId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface IProductCategory {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface IProductBrand {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -22,4 +22,7 @@ export class ProductsService {
|
|||||||
create(data: IProductRequest): Observable<IProductResponse> {
|
create(data: IProductRequest): Observable<IProductResponse> {
|
||||||
return this.http.post<IProductRawResponse>(this.apiRoutes.list(), data);
|
return this.http.post<IProductRawResponse>(this.apiRoutes.list(), data);
|
||||||
}
|
}
|
||||||
|
update(productId: string, data: Partial<IProductRequest>): Observable<IProductResponse> {
|
||||||
|
return this.http.patch<IProductRawResponse>(this.apiRoutes.single(productId), data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<product-full-form [loading]="submitLoading()" (onSubmit)="submit($event)" />
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
|
import { ProductFullFormComponent } from '../components/fullForm/full-form.component';
|
||||||
|
import { IProductRequest } from '../models';
|
||||||
|
import { ProductsService } from '../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-create',
|
||||||
|
templateUrl: './create.component.html',
|
||||||
|
imports: [TextareaModule, ReactiveFormsModule, ProductFullFormComponent],
|
||||||
|
})
|
||||||
|
export class CreateProductComponent {
|
||||||
|
constructor(
|
||||||
|
private service: ProductsService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
submitLoading = signal(false);
|
||||||
|
|
||||||
|
submit(payload: IProductRequest) {
|
||||||
|
this.submitLoading.set(true);
|
||||||
|
this.service.create(payload).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.toastService.success({ text: `محصول ${payload.name} با موفقیت ایجاد شد` });
|
||||||
|
this.router.navigate(['products', res.id]);
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './create.component';
|
||||||
export * from './list.component';
|
export * from './list.component';
|
||||||
export * from './single.component';
|
export * from './single.component';
|
||||||
|
export * from './update.component';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
[loading]="loading()"
|
[loading]="loading()"
|
||||||
[showDetails]="true"
|
[showDetails]="true"
|
||||||
(onAdd)="openAddForm()"
|
(onAdd)="openAddForm()"
|
||||||
|
(onDetails)="toDetails($event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
PageDataListComponent,
|
PageDataListComponent,
|
||||||
} from '@/shared/components/pageDataList/page-data-list.component';
|
} from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
import { Component, signal } from '@angular/core';
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { ProductFormComponent } from '../components/form.component';
|
import { ProductFormComponent } from '../components/form.component';
|
||||||
|
import { productsNamedRoutes } from '../constants';
|
||||||
import { IProductResponse } from '../models';
|
import { IProductResponse } from '../models';
|
||||||
import { ProductsService } from '../services/main.service';
|
import { ProductsService } from '../services/main.service';
|
||||||
|
|
||||||
@@ -13,20 +15,32 @@ import { ProductsService } from '../services/main.service';
|
|||||||
imports: [PageDataListComponent, ProductFormComponent],
|
imports: [PageDataListComponent, ProductFormComponent],
|
||||||
})
|
})
|
||||||
export class ProductsComponent {
|
export class ProductsComponent {
|
||||||
constructor(private productService: ProductsService) {
|
constructor(
|
||||||
|
private productService: ProductsService,
|
||||||
|
private router: Router,
|
||||||
|
) {
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
{ field: 'id', header: 'شناسه' },
|
{ field: 'id', header: 'شناسه' },
|
||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{ field: 'description', header: 'توضیحات' },
|
{
|
||||||
{ field: 'productType', header: 'نوع محصول' },
|
field: 'brand',
|
||||||
{ field: 'brandId', header: 'برند' },
|
header: 'برند',
|
||||||
{ field: 'categoryId', header: 'دستهبندی' },
|
customDataModel(item) {
|
||||||
{ field: 'supplierId', header: 'تامینکننده' },
|
return item.brand?.name || '-';
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
},
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
},
|
||||||
|
{
|
||||||
|
field: 'category',
|
||||||
|
header: 'دستهبندی',
|
||||||
|
customDataModel(item) {
|
||||||
|
return item.category?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
@@ -46,6 +60,14 @@ export class ProductsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openAddForm() {
|
openAddForm() {
|
||||||
this.visibleForm.set(true);
|
const pagePathFn = productsNamedRoutes.create.meta.pagePath;
|
||||||
|
if (pagePathFn) {
|
||||||
|
this.router.navigateByUrl(pagePathFn({}));
|
||||||
|
}
|
||||||
|
// this.visibleForm.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
toDetails(product: IProductResponse) {
|
||||||
|
this.router.navigate(['/products', product.id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,84 @@
|
|||||||
<div class=""></div>
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="grow">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="['/products']" />
|
||||||
|
<h3 class="m-0">{{ product()?.name }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="ویرایش"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
[routerLink]="['/products', productId, 'update']"
|
||||||
|
></button>
|
||||||
|
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex items-center gap-5">
|
||||||
|
<div class="">
|
||||||
|
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<app-key-value label="بارکد" [value]="product()?.barcode || '-'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex grow flex-col gap-4">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
@for (item of topBarCardDetails; track $index) {
|
||||||
|
<p-card class="w-full border border-primary-700">
|
||||||
|
<div class="flex gap-5 text-primary-400">
|
||||||
|
<div class="shrink-0 pt-1">
|
||||||
|
<i [class]="'pi ' + item.icon + ' text-2xl'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="grow flex flex-col gap-1">
|
||||||
|
<span class="text-sm">{{ item.label }}</span>
|
||||||
|
<span class="text-xl font-bold text-primary">{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p-card class="border border-primary-700">
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<span class="font-bold text-lg">توضیحات</span>
|
||||||
|
<p class="m-0">{{ product()?.description || "بدون توضیحات" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 w-xs">
|
||||||
|
<p-card class="border border-primary-700">
|
||||||
|
<div class="flex justify-between p-2 gap-2">
|
||||||
|
<span class="font-bold text-lg">دستهبندی</span>
|
||||||
|
<p class="m-0">{{ product()?.category?.name || "بدون دستهبندی" }}</p>
|
||||||
|
</div>
|
||||||
|
<hr class="m-0" />
|
||||||
|
<div class="flex justify-between p-2 gap-2">
|
||||||
|
<span class="font-bold text-lg">برند</span>
|
||||||
|
<p class="m-0">{{ product()?.brand?.name || "بدون برند" }}</p>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
|
</div>
|
||||||
|
<p-galleria
|
||||||
|
[value]="images()"
|
||||||
|
[showItemNavigators]="true"
|
||||||
|
[circular]="true"
|
||||||
|
[numVisible]="4"
|
||||||
|
class="shrink-0 w-md"
|
||||||
|
containerClass="max-w-md"
|
||||||
|
>
|
||||||
|
<ng-template #item let-item>
|
||||||
|
<img [src]="item.itemImageSrc" style="width: 100%; display: block" />
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #thumbnail let-item>
|
||||||
|
<img [src]="item.itemImageSrc" style="display: block" />
|
||||||
|
</ng-template>
|
||||||
|
</p-galleria>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,95 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { IProductResponse } from '../models';
|
||||||
|
import { ProductsService } from '../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product',
|
selector: 'app-product',
|
||||||
templateUrl: './single.component.html',
|
templateUrl: './single.component.html',
|
||||||
|
imports: [ButtonDirective, RouterLink, GalleriaModule, KeyValueComponent, Card, Button],
|
||||||
})
|
})
|
||||||
export class ProductComponent {
|
export class ProductComponent {
|
||||||
constructor() {}
|
private route = inject(ActivatedRoute);
|
||||||
|
constructor(private service: ProductsService) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
images = signal([
|
||||||
|
{
|
||||||
|
itemImageSrc: 'https://primefaces.org/cdn/primeng/images/galleria/galleria1.jpg',
|
||||||
|
alt: 'Description for Image 1',
|
||||||
|
title: 'Title 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemImageSrc: 'https://primefaces.org/cdn/primeng/images/galleria/galleria2.jpg',
|
||||||
|
alt: 'Description for Image 2',
|
||||||
|
title: 'Title 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemImageSrc: 'https://primefaces.org/cdn/primeng/images/galleria/galleria3.jpg',
|
||||||
|
alt: 'Description for Image 3',
|
||||||
|
title: 'Title 3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemImageSrc: 'https://primefaces.org/cdn/primeng/images/galleria/galleria4.jpg',
|
||||||
|
alt: 'Description for Image 4',
|
||||||
|
title: 'Title 4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemImageSrc: 'https://primefaces.org/cdn/primeng/images/galleria/galleria5.jpg',
|
||||||
|
alt: 'Description for Image 5',
|
||||||
|
title: 'Title 5',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
productId = this.route.snapshot.params['productId'];
|
||||||
|
loading = signal(false);
|
||||||
|
product = signal<Maybe<IProductResponse>>(null);
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.service.getSingle(this.productId).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.product.set(res);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get topBarCardDetails() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: 'pi pi-box',
|
||||||
|
label: 'موجودی',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'pi pi-dollar',
|
||||||
|
label: 'قیمت پایه',
|
||||||
|
value: this.product()?.basePrice?.toString() || 'تعیین نشده',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'pi pi-calendar',
|
||||||
|
label: 'تاریخ ایجاد',
|
||||||
|
value: this.product()?.createdAt
|
||||||
|
? new Date(this.product()!.createdAt!).toLocaleDateString()
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'pi pi-clock',
|
||||||
|
label: 'تعداد فروش',
|
||||||
|
value: 1200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteConfirm() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@if (loading()) {
|
||||||
|
<p-progress-spinner />
|
||||||
|
} @else {
|
||||||
|
<product-full-form
|
||||||
|
[initialData]="product() || undefined"
|
||||||
|
[loading]="submitLoading() || loading()"
|
||||||
|
(onSubmit)="submit($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||||
|
import { ProductFullFormComponent } from '../components/fullForm/full-form.component';
|
||||||
|
import { IProductRequest, IProductResponse } from '../models';
|
||||||
|
import { ProductsService } from '../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-update',
|
||||||
|
templateUrl: './update.component.html',
|
||||||
|
imports: [ProductFullFormComponent, ProgressSpinner],
|
||||||
|
})
|
||||||
|
export class UpdateProductComponent {
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
constructor(
|
||||||
|
private service: ProductsService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private router: Router,
|
||||||
|
) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading = signal(false);
|
||||||
|
|
||||||
|
productId = this.route.snapshot.params['productId'];
|
||||||
|
loading = signal(false);
|
||||||
|
product = signal<Maybe<IProductResponse>>(null);
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.service.getSingle(this.productId).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.product.set(res);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(payload: IProductRequest) {
|
||||||
|
this.submitLoading.set(true);
|
||||||
|
this.service.update(this.productId, payload).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.toastService.success({ text: `محصول ${payload.name} با موفقیت ویرایش شد` });
|
||||||
|
this.router.navigate(['products', this.productId]);
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<uikit-field [control]="control" name="barcode" label="بارکد">
|
||||||
|
<p-input-group>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
type="text"
|
||||||
|
[formControl]="control"
|
||||||
|
name="barcode"
|
||||||
|
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||||
|
/>
|
||||||
|
<p-inputgroup-addon>
|
||||||
|
<button pButton type="button" icon="pi pi-barcode" text (click)="scanBarcode()"></button>
|
||||||
|
</p-inputgroup-addon>
|
||||||
|
</p-input-group>
|
||||||
|
</uikit-field>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { UikitFieldComponent } from '@/uikit';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { InputGroup } from 'primeng/inputgroup';
|
||||||
|
import { InputGroupAddon } from 'primeng/inputgroupaddon';
|
||||||
|
import { InputText } from 'primeng/inputtext';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'barcode-input',
|
||||||
|
templateUrl: './barcode-input.component.html',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
UikitFieldComponent,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
ButtonDirective,
|
||||||
|
InputText,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class BarcodeInputComponent {
|
||||||
|
@Input() control!: FormControl<Maybe<string>>;
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
scanBarcode() {}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
columnResizeMode="fit"
|
columnResizeMode="fit"
|
||||||
stripedRows
|
stripedRows
|
||||||
[showFirstLastIcon]="false"
|
[showFirstLastIcon]="false"
|
||||||
class="grow !flex flex-col overflow-hidden"
|
class="grow flex! flex-col overflow-hidden"
|
||||||
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
|
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
|
||||||
>
|
>
|
||||||
@if (pageTitle || showAdd || filter || caption) {
|
@if (pageTitle || showAdd || filter || caption) {
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
</ng-template> -->
|
</ng-template> -->
|
||||||
|
|
||||||
<ng-template #loadingbody let-columns>
|
<ng-template #loadingbody let-columns>
|
||||||
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; track i) {
|
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
|
||||||
<tr style="height: 46px">
|
<tr style="height: 46px">
|
||||||
@for (col of columns; track col) {
|
@for (col of columns; track col) {
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
computed,
|
computed,
|
||||||
ContentChild,
|
ContentChild,
|
||||||
ElementRef,
|
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
Renderer2,
|
|
||||||
signal,
|
signal,
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@@ -25,6 +23,7 @@ export interface IColumn {
|
|||||||
width?: string;
|
width?: string;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
|
type?: 'text' | 'price' | 'boolean' | 'date';
|
||||||
customDataModel?: TemplateRef<any> | ((item: any) => string | number | boolean);
|
customDataModel?: TemplateRef<any> | ((item: any) => string | number | boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +44,7 @@ export interface IColumn {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PageDataListComponent<I> {
|
export class PageDataListComponent<I> {
|
||||||
constructor(
|
constructor() {}
|
||||||
private host: ElementRef,
|
|
||||||
private renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Input() pageTitle!: string;
|
@Input() pageTitle!: string;
|
||||||
@Input() addNewCtaLabel?: string;
|
@Input() addNewCtaLabel?: string;
|
||||||
@@ -125,6 +121,19 @@ export class PageDataListComponent<I> {
|
|||||||
return this.renderCustom(column, item);
|
return this.renderCustom(column, item);
|
||||||
}
|
}
|
||||||
const data = item[field];
|
const data = item[field];
|
||||||
|
switch (column.type) {
|
||||||
|
case 'date':
|
||||||
|
if (!data) return '-';
|
||||||
|
const date = new Date(data);
|
||||||
|
return isNaN(date.getTime()) ? '-' : date.toLocaleDateString();
|
||||||
|
case 'boolean':
|
||||||
|
return data ? 'بله' : 'خیر';
|
||||||
|
case 'price':
|
||||||
|
return typeof data === 'number' ? data.toLocaleString() : '-';
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (data === undefined || data === null) {
|
if (data === undefined || data === null) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-12
@@ -1,4 +1,4 @@
|
|||||||
import { definePreset } from '@primeuix/themes';
|
import { definePreset, updateSurfacePalette } from '@primeuix/themes';
|
||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
|
|
||||||
// Reusable exported accent color constant
|
// Reusable exported accent color constant
|
||||||
@@ -26,18 +26,65 @@ const MyPreset = definePreset(Aura, {
|
|||||||
},
|
},
|
||||||
semantic: {
|
semantic: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '{indigo.50}',
|
50: '{surface.50}',
|
||||||
100: '{indigo.100}',
|
100: '{surface.100}',
|
||||||
200: '{indigo.200}',
|
200: '{surface.200}',
|
||||||
300: '{indigo.300}',
|
300: '{surface.300}',
|
||||||
400: '{indigo.400}',
|
400: '{surface.400}',
|
||||||
500: '{indigo.500}',
|
500: '{surface.500}',
|
||||||
600: '{indigo.600}',
|
600: '{surface.600}',
|
||||||
700: '{indigo.700}',
|
700: '{surface.700}',
|
||||||
800: '{indigo.800}',
|
800: '{surface.800}',
|
||||||
900: '{indigo.900}',
|
900: '{surface.900}',
|
||||||
950: '{indigo.950}',
|
950: '{surface.950}',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
colorScheme: {
|
||||||
|
light: {
|
||||||
|
primary: {
|
||||||
|
color: '{primary.950}',
|
||||||
|
contrastColor: '#ffffff',
|
||||||
|
hoverColor: '{primary.800}',
|
||||||
|
activeColor: '{primary.700}',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
background: '{primary.950}',
|
||||||
|
focusBackground: '{primary.700}',
|
||||||
|
color: '#ffffff',
|
||||||
|
focusColor: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: {
|
||||||
|
color: '{primary.50}',
|
||||||
|
contrastColor: '{primary.950}',
|
||||||
|
hoverColor: '{primary.200}',
|
||||||
|
activeColor: '{primary.300}',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
background: '{primary.50}',
|
||||||
|
focusBackground: '{primary.300}',
|
||||||
|
color: '{primary.950}',
|
||||||
|
focusColor: '{primary.950}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primitive: {
|
||||||
|
...updateSurfacePalette({
|
||||||
|
0: '#ffffff',
|
||||||
|
50: '#f8fafc',
|
||||||
|
100: '#f1f5f9',
|
||||||
|
200: '#e2e8f0',
|
||||||
|
300: '#cbd5e1',
|
||||||
|
400: '#94a3b8',
|
||||||
|
500: '#64748b',
|
||||||
|
600: '#475569',
|
||||||
|
700: '#334155',
|
||||||
|
800: '#1e293b',
|
||||||
|
900: '#0f172a',
|
||||||
|
950: '#020617',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user