feat: implement stock keeping unit management form and list components

- Added StockKeepingUnitFormComponent for creating and editing stock keeping units.
- Introduced StockKeepingUnitsComponent for listing stock keeping units with pagination.
- Integrated form fields for code, name, VAT, and SKU type in the stock keeping unit form.
- Created API routes for stock keeping units with CRUD operations.
- Updated input components to handle numeric and price types with Persian/Arabic digit normalization.
- Enhanced key-value component to support tag display instead of badge.
- Adjusted layout styles for sidebar menu.
- Added AGENT.md for repository-specific coding guidelines and practices.
- Implemented good management form with file upload and category selection.
This commit is contained in:
2026-05-06 22:01:20 +03:30
parent 54d00e19ae
commit b2a1eb8e5b
87 changed files with 882 additions and 434 deletions
@@ -0,0 +1,19 @@
<shared-dialog [header]="preparedTitle" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }"
[closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
@if (visible) {
<app-shared-upload-file accept="image/*" name="image" [initial_file_url]="initialValues?.image_url || ''"
(onSelect)="changeFile($event)" />
}
<field-name [control]="form.controls.name" name="name" />
<field-sku label="شناسه کالا" [control]="form.controls.sku_id" name="sku_id" />
<catalog-guild-goodCategories-select [guildId]="guildId" label="دسته‌بندی" [control]="form.controls.category_id"
name="category_id" />
<catalog-measure-units-select label="واحد اندازه‌گیری" [control]="form.controls.measure_unit_id"
name="measure_unit_id" />
<app-enum-select type="goodPricingModel" [control]="form.controls.pricing_model" name="pricing_model" />
<field-description [control]="form.controls.description" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form>
</shared-dialog>
@@ -0,0 +1,76 @@
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { DescriptionComponent, NameComponent, SkuComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, Input, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Maybe } from '@/core';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { CatalogGuildGoodCategoriesSelectComponent } from '@/shared/catalog/guildGoodCategories';
import { CatalogMeasureUnitsSelectComponent } from '@/shared/catalog/measureUnits';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { onSelectFileArgs } from '@/shared/components/uploadFile/model';
import { fieldControl } from '@/shared/constants';
import { buildFormData } from '@/utils';
import { Observable } from 'rxjs';
import { SharedUploadFileComponent } from '../uploadFile/upload-file.component';
import { IGoodRequestPayload, IGoodResponse } from './types';
@Component({
selector: 'shared-good-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
SharedDialogComponent,
FormFooterActionsComponent,
EnumSelectComponent,
SkuComponent,
CatalogMeasureUnitsSelectComponent,
NameComponent,
DescriptionComponent,
SharedUploadFileComponent,
CatalogGuildGoodCategoriesSelectComponent,
],
})
export class SharedGoodFormComponent extends AbstractFormDialog<
IGoodRequestPayload,
IGoodResponse
> {
@Input({ required: true }) guildId!: string;
@Input({ required: true }) createFn!: (payload: FormData) => Observable<IGoodResponse>;
@Input({ required: true }) updateFn!: (payload: FormData) => Observable<IGoodResponse>;
goodImage = signal<Maybe<File>>(null);
form = this.fb.group({
name: fieldControl.name(this.initialValues?.name || '', true),
sku_id: fieldControl.sku(this.initialValues?.sku.id || '', true),
measure_unit_id: fieldControl.measure_unit(this.initialValues?.measure_unit.id || '', true),
pricing_model: fieldControl.pricing_model(this.initialValues?.pricing_model || '', true),
category_id: fieldControl.category_id(this.initialValues?.category.id || '', true),
description: fieldControl.description(this.initialValues?.description || '', false),
});
get preparedTitle() {
return `${this.editMode ? 'ویرایش' : 'افزودن'} کالا`;
}
changeFile(payload: onSelectFileArgs) {
this.goodImage.set(payload.file);
}
override ngOnChanges(): void {
this.form.patchValue(this.initialValues || {});
this.form.controls.category_id.setValue(this.initialValues?.category.id || '');
this.form.controls.measure_unit_id.setValue(this.initialValues?.measure_unit.id || '');
this.form.controls.sku_id.setValue(this.initialValues?.sku.id || '');
}
override submitForm() {
const formData = buildFormData(this.form, { image: this.goodImage() });
if (this.editMode) {
return this.updateFn(formData);
}
return this.createFn(formData);
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './form.component';
export * from './types';
+23
View File
@@ -0,0 +1,23 @@
import ISummary from '@/core/models/summary';
export interface IGoodResponse {
id: string;
name: string;
image_url: string;
sku: ISummary;
category: ISummary;
measure_unit: ISummary;
pricing_model: string;
description?: string;
created_at: string;
updated_at: string;
}
export interface IGoodRequestPayload {
name: string;
sku_id: string;
category_id: string;
measure_unit_id: string;
pricing_model: string;
description?: string;
}