feat: implement product creation and update features with form handling and routing
This commit is contained in:
@@ -2,7 +2,6 @@ import { ProductBrandsSelectComponent } from '@/modules/productBrands/components
|
||||
import { ProductCategoriesSelectComponent } from '@/modules/productCategories/components';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
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 { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Dialog } from 'primeng/dialog';
|
||||
@@ -17,7 +16,6 @@ import { ProductsService } from '../services/main.service';
|
||||
Dialog,
|
||||
InputComponent,
|
||||
FormFooterActionsComponent,
|
||||
UikitFieldComponent,
|
||||
ProductBrandsSelectComponent,
|
||||
ProductCategoriesSelectComponent,
|
||||
],
|
||||
@@ -49,8 +47,8 @@ export class ProductFormComponent {
|
||||
name: [this.initialValues?.name || null, [Validators.required]],
|
||||
description: [this.initialValues?.description || null],
|
||||
// productType: [this.initialValues?.productType || null, [Validators.required]],
|
||||
brandId: [this.initialValues?.brandId || null, [Validators.required]],
|
||||
categoryId: [this.initialValues?.categoryId || null],
|
||||
brandId: [this.initialValues?.brand?.id || null, [Validators.required]],
|
||||
categoryId: [this.initialValues?.category?.id || null],
|
||||
});
|
||||
|
||||
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 { Routes } from '@angular/router';
|
||||
|
||||
export type TProductsRouteNames = 'products' | 'product';
|
||||
export type TProductsRouteNames = 'products' | 'product' | 'create' | 'update';
|
||||
|
||||
export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
||||
products: {
|
||||
@@ -12,6 +12,15 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
||||
pagePath: () => '/products',
|
||||
},
|
||||
},
|
||||
create: {
|
||||
path: 'products/create',
|
||||
loadComponent: () =>
|
||||
import('../../views/create.component').then((m) => m.CreateProductComponent),
|
||||
meta: {
|
||||
title: 'ایجاد محصول جدید',
|
||||
pagePath: () => '/products/create',
|
||||
},
|
||||
},
|
||||
product: {
|
||||
path: 'products/:productId',
|
||||
loadComponent: () => import('../../views/single.component').then((m) => m.ProductComponent),
|
||||
@@ -20,6 +29,15 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
||||
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);
|
||||
|
||||
+18
-10
@@ -1,23 +1,31 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { IProductBrand, IProductCategory } from './others';
|
||||
|
||||
export interface IProductRawResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
productType: string;
|
||||
description?: string;
|
||||
barcode?: Maybe<string>;
|
||||
sku?: Maybe<string>;
|
||||
// productType: string;
|
||||
brand?: IProductBrand;
|
||||
category?: IProductCategory;
|
||||
// supplierId: number;
|
||||
basePrice?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date;
|
||||
brandId: number;
|
||||
categoryId: number;
|
||||
supplierId: number;
|
||||
deletedAt?: Maybe<Date>;
|
||||
}
|
||||
|
||||
export interface IProductResponse extends IProductRawResponse {}
|
||||
|
||||
export interface IProductRequest {
|
||||
name: string;
|
||||
description: string;
|
||||
productType: string;
|
||||
// productType: string;
|
||||
brandId: number;
|
||||
categoryId: number;
|
||||
supplierId: number;
|
||||
description: Maybe<string>;
|
||||
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> {
|
||||
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 './single.component';
|
||||
export * from './update.component';
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[loading]="loading()"
|
||||
[showDetails]="true"
|
||||
(onAdd)="openAddForm()"
|
||||
(onDetails)="toDetails($event)"
|
||||
/>
|
||||
|
||||
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
PageDataListComponent,
|
||||
} from '@/shared/components/pageDataList/page-data-list.component';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProductFormComponent } from '../components/form.component';
|
||||
import { productsNamedRoutes } from '../constants';
|
||||
import { IProductResponse } from '../models';
|
||||
import { ProductsService } from '../services/main.service';
|
||||
|
||||
@@ -13,20 +15,32 @@ import { ProductsService } from '../services/main.service';
|
||||
imports: [PageDataListComponent, ProductFormComponent],
|
||||
})
|
||||
export class ProductsComponent {
|
||||
constructor(private productService: ProductsService) {
|
||||
constructor(
|
||||
private productService: ProductsService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.getData();
|
||||
}
|
||||
|
||||
columns = [
|
||||
{ field: 'id', header: 'شناسه' },
|
||||
{ field: 'name', header: 'نام' },
|
||||
{ field: 'description', header: 'توضیحات' },
|
||||
{ field: 'productType', header: 'نوع محصول' },
|
||||
{ field: 'brandId', header: 'برند' },
|
||||
{ field: 'categoryId', header: 'دستهبندی' },
|
||||
{ field: 'supplierId', header: 'تامینکننده' },
|
||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
||||
{
|
||||
field: 'brand',
|
||||
header: 'برند',
|
||||
customDataModel(item) {
|
||||
return item.brand?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
header: 'دستهبندی',
|
||||
customDataModel(item) {
|
||||
return item.category?.name || '-';
|
||||
},
|
||||
},
|
||||
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||
] as IColumn[];
|
||||
|
||||
loading = signal(false);
|
||||
@@ -46,6 +60,14 @@ export class ProductsComponent {
|
||||
}
|
||||
|
||||
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({
|
||||
selector: 'app-product',
|
||||
templateUrl: './single.component.html',
|
||||
imports: [ButtonDirective, RouterLink, GalleriaModule, KeyValueComponent, Card, Button],
|
||||
})
|
||||
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"
|
||||
stripedRows
|
||||
[showFirstLastIcon]="false"
|
||||
class="grow !flex flex-col overflow-hidden"
|
||||
class="grow flex! flex-col overflow-hidden"
|
||||
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
|
||||
>
|
||||
@if (pageTitle || showAdd || filter || caption) {
|
||||
@@ -102,7 +102,7 @@
|
||||
</ng-template> -->
|
||||
|
||||
<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">
|
||||
@for (col of columns; track col) {
|
||||
<td>
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
signal,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
@@ -25,6 +23,7 @@ export interface IColumn {
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: 'text' | 'price' | 'boolean' | 'date';
|
||||
customDataModel?: TemplateRef<any> | ((item: any) => string | number | boolean);
|
||||
}
|
||||
|
||||
@@ -45,10 +44,7 @@ export interface IColumn {
|
||||
],
|
||||
})
|
||||
export class PageDataListComponent<I> {
|
||||
constructor(
|
||||
private host: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
constructor() {}
|
||||
|
||||
@Input() pageTitle!: string;
|
||||
@Input() addNewCtaLabel?: string;
|
||||
@@ -125,6 +121,19 @@ export class PageDataListComponent<I> {
|
||||
return this.renderCustom(column, item);
|
||||
}
|
||||
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) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
+59
-12
@@ -1,4 +1,4 @@
|
||||
import { definePreset } from '@primeuix/themes';
|
||||
import { definePreset, updateSurfacePalette } from '@primeuix/themes';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
|
||||
// Reusable exported accent color constant
|
||||
@@ -26,18 +26,65 @@ const MyPreset = definePreset(Aura, {
|
||||
},
|
||||
semantic: {
|
||||
primary: {
|
||||
50: '{indigo.50}',
|
||||
100: '{indigo.100}',
|
||||
200: '{indigo.200}',
|
||||
300: '{indigo.300}',
|
||||
400: '{indigo.400}',
|
||||
500: '{indigo.500}',
|
||||
600: '{indigo.600}',
|
||||
700: '{indigo.700}',
|
||||
800: '{indigo.800}',
|
||||
900: '{indigo.900}',
|
||||
950: '{indigo.950}',
|
||||
50: '{surface.50}',
|
||||
100: '{surface.100}',
|
||||
200: '{surface.200}',
|
||||
300: '{surface.300}',
|
||||
400: '{surface.400}',
|
||||
500: '{surface.500}',
|
||||
600: '{surface.600}',
|
||||
700: '{surface.700}',
|
||||
800: '{surface.800}',
|
||||
900: '{surface.900}',
|
||||
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