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:
@@ -0,0 +1,117 @@
|
||||
# AGENT.md
|
||||
|
||||
## Purpose
|
||||
|
||||
- This file defines repository-specific instructions for coding agents.
|
||||
- Scope is the full repo unless a deeper `AGENT.md` overrides it.
|
||||
|
||||
## Stack Context
|
||||
|
||||
- Frontend: Angular 20 standalone app.
|
||||
- Package manager: `pnpm`.
|
||||
- Deployment: Docker / Docker Compose with tenant-specific services.
|
||||
- Current service mapping expectation:
|
||||
- `app_default` on host port `8090`
|
||||
- `app_tis` on host port `8091`
|
||||
|
||||
## Tenant Build Rules
|
||||
|
||||
- `default` tenant currently builds via `ng build` and outputs to `dist/production`.
|
||||
- `tis` tenant builds via `ng build --configuration tis` and outputs to `dist/tis`.
|
||||
- Keep Docker `DIST_DIR` aligned with actual Angular output path.
|
||||
- Do not assume `default` Angular configuration is usable unless verified (it may reference missing replacements).
|
||||
|
||||
## Input Component Rules
|
||||
|
||||
- File: `src/app/shared/components/input/input.component.ts`
|
||||
- For `type === 'number'` or `type === 'price'`:
|
||||
- Normalize Persian/Arabic digits to English digits.
|
||||
- Allow only digits and `.`.
|
||||
- Support `fixed` precision formatting when provided.
|
||||
- Keep behavior for identifier fields (`mobile`, `phone`, `postalCode`, `nationalId`) string-safe.
|
||||
|
||||
## Change Policy
|
||||
|
||||
- Keep changes minimal and scoped to user request.
|
||||
- Prefer root-cause fixes over temporary workarounds.
|
||||
- Avoid unrelated refactors.
|
||||
- Reuse existing patterns and naming conventions.
|
||||
|
||||
## Do / Don't
|
||||
|
||||
- Do follow existing field wrapper style in `src/app/shared/components/fields/*.component.ts`.
|
||||
- Do reuse `app-input` and set only required props (`type`, `label`, `name`, constraints).
|
||||
- Do register every new field in:
|
||||
- `src/app/shared/components/fields/index.ts`
|
||||
- `src/app/shared/constants/fields/index.ts`
|
||||
- Do keep control keys consistent across form group, field component `name`, and `fieldControl` key.
|
||||
- Don't add one-off field patterns when an existing field component can be reused.
|
||||
- Don't use invalid Angular file replacements for directories or empty paths.
|
||||
- Don't change tenant output directories without updating Docker `DIST_DIR`.
|
||||
|
||||
## How To Create Form Fields
|
||||
|
||||
- Create a wrapper component in `src/app/shared/components/fields`.
|
||||
- Use the same pattern as existing files like:
|
||||
- `src/app/shared/components/fields/name.component.ts`
|
||||
- `src/app/shared/components/fields/unit_price.component.ts`
|
||||
- Minimal wrapper shape:
|
||||
- `selector`: `field-<field-name>`
|
||||
- template: `<app-input ... />`
|
||||
- inputs: `control` (required), optional `name`, optional `label`
|
||||
|
||||
Example pattern:
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'field-example',
|
||||
template: `<app-input [label]="label" [control]="control" [name]="name" type="simple" />`,
|
||||
imports: [ReactiveFormsModule, InputComponent],
|
||||
})
|
||||
export class ExampleComponent {
|
||||
@Input({ required: true }) control = new FormControl<string>('');
|
||||
@Input() name = 'example';
|
||||
@Input() label = 'Example';
|
||||
}
|
||||
```
|
||||
|
||||
## Register New Fields
|
||||
|
||||
- Export the component from:
|
||||
- `src/app/shared/components/fields/index.ts`
|
||||
- Add its form control factory in:
|
||||
- `src/app/shared/constants/fields/index.ts`
|
||||
- `fieldControl` entry shape:
|
||||
- key must match form control name
|
||||
- return tuple: `[defaultValue, validators]`
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
example: (value = '', isRequired = true): ControlConfig => [
|
||||
value,
|
||||
isRequired ? [Validators.required] : [],
|
||||
],
|
||||
```
|
||||
|
||||
## Using Fields In Forms
|
||||
|
||||
- In form group builders, use `fieldControl.<key>(initialValue, isRequired)` for consistency.
|
||||
- In templates, render matching wrapper component and pass the matching control:
|
||||
- `<field-example [control]="form.controls.example" />`
|
||||
- For numeric/price behavior, use `app-input` `type="number"` or `type="price"` and optional `[fixed]`.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- For TypeScript-only changes, run:
|
||||
- `pnpm -s exec tsc -p tsconfig.app.json --noEmit`
|
||||
- For Docker/build changes, verify with:
|
||||
- `docker compose build app_default`
|
||||
- `docker compose build app_tis`
|
||||
- Start with targeted validation, then broader checks only if needed.
|
||||
|
||||
## Communication Expectations
|
||||
|
||||
- Report exactly which files changed and why.
|
||||
- Call out any assumptions or discovered config mismatches.
|
||||
- If validation is blocked (permissions, missing dependencies), state it clearly and provide next command.
|
||||
@@ -4,9 +4,10 @@ import { Maybe } from '../models';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
interface INativeBridgeHost {
|
||||
pay?: ((payload: string) => unknown) | ((amount: number, id: Maybe<string>) => unknown);
|
||||
print?: (payload: string) => unknown;
|
||||
pay?: (amount: number, id: Maybe<string>) => unknown;
|
||||
print?: (payload: INativePrintRequest) => unknown;
|
||||
isEnabled?: () => boolean;
|
||||
getUUID?: () => Maybe<string>;
|
||||
}
|
||||
|
||||
export interface INativePayRequest {
|
||||
@@ -15,8 +16,11 @@ export interface INativePayRequest {
|
||||
}
|
||||
|
||||
export interface INativePrintRequest {
|
||||
invoiceId?: string;
|
||||
code?: string;
|
||||
title: string;
|
||||
items: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface INativeBridgeResult<T = unknown> {
|
||||
@@ -31,15 +35,14 @@ export class NativeBridgeService {
|
||||
|
||||
private get host(): INativeBridgeHost | undefined {
|
||||
const globalWindow = window as unknown as {
|
||||
AndroidBridge?: INativeBridgeHost;
|
||||
Android?: INativeBridgeHost;
|
||||
AndroidPSP?: INativeBridgeHost;
|
||||
NativeBridge?: INativeBridgeHost;
|
||||
};
|
||||
|
||||
return globalWindow.AndroidPSP || globalWindow.AndroidBridge || globalWindow.Android;
|
||||
return globalWindow.NativeBridge;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
if (!this.host) return false;
|
||||
const hostEnabled = this.host?.isEnabled;
|
||||
if (typeof hostEnabled === 'function') {
|
||||
try {
|
||||
@@ -60,27 +63,20 @@ export class NativeBridgeService {
|
||||
return this.isEnabled() && typeof this.host?.print === 'function';
|
||||
}
|
||||
|
||||
pay(request: INativePayRequest): INativeBridgeResult {
|
||||
pay(request: INativePayRequest): any {
|
||||
this.toastService.info({ text: 'در حال پردازش پرداخت...', life: 3000 });
|
||||
const fn = this.host?.pay;
|
||||
if (typeof fn !== 'function') {
|
||||
return { success: false, error: 'Native method "pay" is not available.' };
|
||||
this.toastService.error({ text: 'متاسفانه پرداخت امکانپذیر نیست.', life: 3000 });
|
||||
return { success: false, error: 'متاسفانه پرداخت امکانپذیر نیست.' };
|
||||
}
|
||||
|
||||
try {
|
||||
let raw: unknown;
|
||||
if (fn.length >= 2) {
|
||||
raw = (fn as (amount: number, id: Maybe<string>) => unknown)(request.amount, request.id);
|
||||
} else {
|
||||
raw = (fn as (payload: string) => unknown)(JSON.stringify(request));
|
||||
}
|
||||
fn(request.amount, request.id);
|
||||
|
||||
const parsed = this.tryParse(raw);
|
||||
if (parsed && typeof parsed === 'object' && 'success' in parsed) {
|
||||
return parsed as INativeBridgeResult;
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
return { success: true, data: parsed ?? raw };
|
||||
// return { success: true, data: parsed ?? raw };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
@@ -90,25 +86,25 @@ export class NativeBridgeService {
|
||||
return this.invokePrint(request);
|
||||
}
|
||||
|
||||
private invokePrint(payload: object): INativeBridgeResult {
|
||||
private invokePrint(payload: INativePrintRequest): INativeBridgeResult {
|
||||
const fn = this.host?.print;
|
||||
if (typeof fn !== 'function') {
|
||||
return { success: false, error: 'Native method "print" is not available.' };
|
||||
return { success: false, error: 'متاسفانه چاپ امکانپذیر نیست.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fn(JSON.stringify(payload));
|
||||
const parsed = this.tryParse(raw);
|
||||
if (parsed && typeof parsed === 'object' && 'success' in parsed) {
|
||||
return parsed as INativeBridgeResult;
|
||||
}
|
||||
fn(payload);
|
||||
|
||||
return { success: true, data: parsed ?? raw };
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
return { success: false, error: 'متاسفانه چاپ امکانپذیر نیست.' };
|
||||
}
|
||||
}
|
||||
|
||||
getNativeDeviceId(): Maybe<string> {
|
||||
return typeof this.host?.getUUID === 'function' ? this.host.getUUID!() : null;
|
||||
}
|
||||
|
||||
private tryParse(value: unknown): unknown {
|
||||
if (typeof value !== 'string') return value;
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import { CatalogTaxProviderStatusTagComponent } from '@/shared/catalog';
|
||||
import { AppCardComponent, KeyValueComponent } from '@/shared/components';
|
||||
@@ -47,7 +46,7 @@ export class ConsumerSaleInvoiceSharedComponent {
|
||||
private readonly nativeBridge = inject(NativeBridgeService);
|
||||
|
||||
@Input({ required: true }) loading!: boolean;
|
||||
@Input() invoice?: Maybe<ISaleInvoiceFullResponse>;
|
||||
@Input({ required: true }) invoice!: ISaleInvoiceFullResponse;
|
||||
@Input() variant: TConsumerSaleInvoice = 'full';
|
||||
@Input() backRoute?: UrlTree | string | any[];
|
||||
|
||||
@@ -58,8 +57,13 @@ export class ConsumerSaleInvoiceSharedComponent {
|
||||
printInvoice = () => {
|
||||
if (this.nativeBridge.isEnabled()) {
|
||||
const result = this.nativeBridge.print({
|
||||
invoiceId: this.invoice?.id,
|
||||
code: this.invoice?.code,
|
||||
title: 'salam',
|
||||
items: [
|
||||
{
|
||||
label: 'مجموع قیمت',
|
||||
value: this.invoice?.total_amount,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (result.success) return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { IPosInitialValues } from './pos.model';
|
||||
|
||||
export const columns: IColumn[] = [
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
// // { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'serial_number', header: 'شماره سریال' },
|
||||
{ field: 'pos_type', header: 'نوع دستگاه' },
|
||||
|
||||
@@ -24,7 +24,7 @@ export class ConsumerAccountsComponent extends AbstractList<IAccountResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'username',
|
||||
header: 'نام کاربری',
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export class ConsumerComplexListComponent extends AbstractList<IComplexResponse>
|
||||
@Input({ required: true }) businessId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'branch_code', header: 'کد شعبه' },
|
||||
{
|
||||
|
||||
+11
-17
@@ -1,17 +1,11 @@
|
||||
<shared-dialog [header]="preparedTitle" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
<app-input label="عنوان" [control]="form.controls.name" name="name" />
|
||||
<app-input label="شناسه عمومی" [control]="form.controls.sku" name="sku" />
|
||||
<catalog-guild-goodCategories-select
|
||||
[guildId]="guildId"
|
||||
label="دستهبندی"
|
||||
[control]="form.controls.category_id"
|
||||
name="category_id"
|
||||
/>
|
||||
<app-enum-select type="unitType" [control]="form.controls.unit_type" />
|
||||
<app-enum-select type="goodPricingModel" [control]="form.controls.pricing_model" />
|
||||
<app-input label="توضیحات" [control]="form.controls.description" name="description" />
|
||||
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||
</form>
|
||||
</shared-dialog>
|
||||
<shared-good-form
|
||||
[guildId]="guildId"
|
||||
[visible]="visible"
|
||||
(visibleChange)="visibleChange.emit($event)"
|
||||
[initialValues]="initialValues"
|
||||
[editMode]="editMode"
|
||||
[createFn]="create"
|
||||
[updateFn]="update"
|
||||
(onSubmit)="onSubmit.emit($event)"
|
||||
(onClose)="onClose.emit()"
|
||||
/>
|
||||
|
||||
+16
-42
@@ -1,57 +1,31 @@
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
|
||||
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
|
||||
import { CatalogGuildGoodCategoriesSelectComponent } from '@/shared/catalog/guildGoodCategories';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import {
|
||||
IConsumerBusinessActivityGoodRequest,
|
||||
IConsumerBusinessActivityGoodResponse,
|
||||
} from '../../models/goods_io';
|
||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||
import { IGoodResponse, SharedGoodFormComponent } from '@/shared/components/good';
|
||||
import { IConsumerBusinessActivityGoodResponse } from '../../models/goods_io';
|
||||
import { BusinessActivityGoodsService } from '../../services/goods.service';
|
||||
|
||||
@Component({
|
||||
selector: 'consumer-businessActivity-good-form',
|
||||
templateUrl: './form.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
SharedDialogComponent,
|
||||
InputComponent,
|
||||
FormFooterActionsComponent,
|
||||
EnumSelectComponent,
|
||||
CatalogGuildGoodCategoriesSelectComponent,
|
||||
],
|
||||
imports: [SharedGoodFormComponent],
|
||||
})
|
||||
export class ConsumerBusinessActivityGoodFormComponent extends AbstractFormDialog<
|
||||
IConsumerBusinessActivityGoodRequest,
|
||||
IConsumerBusinessActivityGoodResponse
|
||||
> {
|
||||
export class ConsumerBusinessActivityGoodFormComponent extends AbstractDialog {
|
||||
@Input({ required: true }) businessId!: string;
|
||||
@Input({ required: true }) guildId!: string;
|
||||
@Input() categoryId?: string;
|
||||
@Input() initialValues?: IConsumerBusinessActivityGoodResponse;
|
||||
@Input() editMode?: boolean;
|
||||
|
||||
@Output() onSubmit = new EventEmitter<IGoodResponse>();
|
||||
|
||||
private service = inject(BusinessActivityGoodsService);
|
||||
|
||||
form = this.fb.group({
|
||||
name: [this.initialValues?.name || '', [Validators.required]],
|
||||
sku: [this.initialValues?.sku || '', [Validators.required]],
|
||||
unit_type: [this.initialValues?.unit_type || '', [Validators.required]],
|
||||
pricing_model: [this.initialValues?.pricing_model || '', [Validators.required]],
|
||||
category_id: [this.initialValues?.category.id || '', [Validators.required]],
|
||||
description: [this.initialValues?.description || ''],
|
||||
});
|
||||
|
||||
get preparedTitle() {
|
||||
return `${this.editMode ? 'ویرایش' : 'افزودن'} کالا`;
|
||||
}
|
||||
|
||||
override submitForm(payload: IConsumerBusinessActivityGoodRequest) {
|
||||
if (this.editMode) {
|
||||
return this.service.update(this.businessId, this.categoryId!, payload);
|
||||
}
|
||||
create = (payload: FormData) => {
|
||||
return this.service.create(this.businessId, payload);
|
||||
}
|
||||
};
|
||||
|
||||
update = (payload: FormData) => {
|
||||
return this.service.update(this.businessId, this.categoryId!, payload);
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export class ConsumerBusinessActivityGoodsListComponent extends AbstractList<ICo
|
||||
@Input({ required: true }) guildId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'sku', header: 'شناسه عمومی' },
|
||||
{
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export class ConsumerSalesInvoicesComponent extends AbstractList<ISalesInvoicesR
|
||||
@Input({ required: true }) posId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'code', header: 'کد رهگیری' },
|
||||
{ field: 'total_amount', header: 'مبلغ کل', type: 'price' },
|
||||
{
|
||||
|
||||
@@ -3,9 +3,10 @@ import ISummary from '@/core/models';
|
||||
export interface IConsumerBusinessActivityGoodRawResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
image_url: string;
|
||||
sku: ISummary;
|
||||
category: ISummary;
|
||||
unit_type: string;
|
||||
measure_unit: ISummary;
|
||||
pricing_model: string;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
@@ -15,9 +16,9 @@ export interface IConsumerBusinessActivityGoodResponse extends IConsumerBusiness
|
||||
|
||||
export interface IConsumerBusinessActivityGoodRequest {
|
||||
name: string;
|
||||
sku: string;
|
||||
sku_id: string;
|
||||
category_id: string;
|
||||
unit_type: string;
|
||||
measure_unit_id: string;
|
||||
pricing_model: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Observable } from 'rxjs';
|
||||
import { CONSUMER_BUSINESS_ACTIVITY_GOODS_API_ROUTES } from '../constants/apiRoutes/businessActivityGoods';
|
||||
import {
|
||||
IConsumerBusinessActivityGoodRawResponse,
|
||||
IConsumerBusinessActivityGoodRequest,
|
||||
IConsumerBusinessActivityGoodResponse,
|
||||
} from '../models/goods_io';
|
||||
|
||||
@@ -28,10 +27,7 @@ export class BusinessActivityGoodsService {
|
||||
);
|
||||
}
|
||||
|
||||
create(
|
||||
businessId: string,
|
||||
data: IConsumerBusinessActivityGoodRequest,
|
||||
): Observable<IConsumerBusinessActivityGoodResponse> {
|
||||
create(businessId: string, data: FormData): Observable<IConsumerBusinessActivityGoodResponse> {
|
||||
return this.http.post<IConsumerBusinessActivityGoodResponse>(
|
||||
this.apiRoutes.list(businessId),
|
||||
data,
|
||||
@@ -41,7 +37,7 @@ export class BusinessActivityGoodsService {
|
||||
update(
|
||||
businessId: string,
|
||||
id: string,
|
||||
data: IConsumerBusinessActivityGoodRequest,
|
||||
data: FormData,
|
||||
): Observable<IConsumerBusinessActivityGoodResponse> {
|
||||
return this.http.patch<IConsumerBusinessActivityGoodResponse>(
|
||||
this.apiRoutes.single(businessId, id),
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ConsumerBusinessActivitiesComponent extends AbstractList<IBusinessA
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{
|
||||
field: 'created_at',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ICustomerResponse } from '../models';
|
||||
import { ConsumerCustomerIndividualFormComponent } from './form-individual.component';
|
||||
import { ConsumerCustomerLegalFormComponent } from './form-legal.component';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'consumer-customer-form-dialog',
|
||||
|
||||
@@ -19,7 +19,7 @@ import { CustomersService } from '../services/main.service';
|
||||
export class ConsumerCustomerListComponent extends AbstractList<ICustomerResponse> {
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'type',
|
||||
header: 'نوع مشتری',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EntityState, EntityStore } from '@/core/state';
|
||||
import { computed, inject, Injectable } from '@angular/core';
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { catchError, finalize } from 'rxjs';
|
||||
import { consumerPosesNamedRoutes } from '../../businessActivities/constants/routes/poses';
|
||||
import { ConsumerPosesNamedRoutes } from '../constants/routes/index';
|
||||
import { IPosResponse } from '../models';
|
||||
import { ConsumerPosesService } from '../services/main.service';
|
||||
|
||||
@@ -32,12 +32,12 @@ export class ConsumerPosStore extends EntityStore<IPosResponse, ConsumerPosState
|
||||
this.patchState({
|
||||
breadcrumbItems: [
|
||||
{
|
||||
...consumerPosesNamedRoutes.poses.meta,
|
||||
routerLink: consumerPosesNamedRoutes.poses.meta.pagePath!(),
|
||||
...ConsumerPosesNamedRoutes.poses.meta,
|
||||
routerLink: ConsumerPosesNamedRoutes.poses.meta.pagePath!(),
|
||||
},
|
||||
{
|
||||
title: this.entity()?.name,
|
||||
routerLink: consumerPosesNamedRoutes.pos.meta.pagePath!(posId),
|
||||
routerLink: ConsumerPosesNamedRoutes.pos.meta.pagePath!(posId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export class PartnerAccountsComponent extends AbstractList<IAccountResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'username',
|
||||
header: 'نام کاربری',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[columns]="columns"
|
||||
emptyPlaceholderTitle="حساب کاربریای یافت نشد."
|
||||
[items]="items()"
|
||||
[showIndex]="false"
|
||||
[loading]="loading()"
|
||||
(onRefresh)="refresh()"
|
||||
>
|
||||
|
||||
@@ -18,7 +18,6 @@ export class ConsumerAccountListComponent extends AbstractList<IConsumerAccountR
|
||||
@Input({ required: true }) consumerId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'account',
|
||||
header: 'نام کاربری',
|
||||
@@ -41,6 +40,7 @@ export class ConsumerAccountListComponent extends AbstractList<IConsumerAccountR
|
||||
header: 'وضعیت',
|
||||
type: 'nested',
|
||||
nestedOption: { path: 'account.status.translate' },
|
||||
variant: 'tag',
|
||||
},
|
||||
// {
|
||||
// field: 'created_at',
|
||||
|
||||
+1
@@ -19,6 +19,7 @@
|
||||
@case (1) {
|
||||
<partner-consumer-businessActivities-form-content
|
||||
[consumerId]="consumerId"
|
||||
[visible]="visible"
|
||||
(onSubmit)="onBusinessActivitySubmit($event)"
|
||||
(onClose)="close()"
|
||||
/>
|
||||
|
||||
+1
@@ -11,6 +11,7 @@
|
||||
[businessActivityId]="businessActivityId"
|
||||
[initialValues]="initialValues"
|
||||
[editMode]="editMode"
|
||||
[visible]="visible"
|
||||
(onSubmit)="onFormSubmit($event)"
|
||||
(onClose)="close()"
|
||||
/>
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractForm<
|
||||
|
||||
@Input({ required: true }) consumerId!: string;
|
||||
@Input() businessActivityId!: string;
|
||||
@Input({ required: true }) visible!: boolean;
|
||||
|
||||
initForm = () => {
|
||||
const form = this.fb.group({
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ConsumerComplexesComponent extends AbstractList<IComplexResponse> {
|
||||
@Input() businessId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'branch_code', header: 'کد شعبه' },
|
||||
{ field: 'pos_count', header: 'تعداد پایانهها' },
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ConsumerPosesComponent extends AbstractList<IPosResponse> {
|
||||
@Input({ required: true }) complexId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
// { field: 'serial_number', header: 'شماره سریال' },
|
||||
// { field: 'device', header: 'دستگاه', type: 'nested', nestedOption: { path: 'device.name' } },
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<app-key-value label="شماره موبایل" [value]="consumer()?.individual?.mobile_number" />
|
||||
<app-key-value label="کد ملی" [value]="consumer()?.individual?.national_code" />
|
||||
}
|
||||
<app-key-value label="وضعیت" [value]="consumer()?.status?.translate" />
|
||||
<app-key-value label="وضعیت" [value]="consumer()?.status?.value === 'ACTIVE'" type="active" />
|
||||
</div>
|
||||
</div>
|
||||
</app-card-data>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { CustomersService } from '../services/main.service';
|
||||
export class PartnerCustomerListComponent extends AbstractList<ICustomerResponse> {
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
|
||||
{
|
||||
field: 'first_name',
|
||||
|
||||
@@ -15,7 +15,7 @@ export class PartnerLicenseListComponent extends AbstractList<ILicenseResponse>
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'business_activity',
|
||||
header: 'عنوان مشتری',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<ng-template #topbarStart>
|
||||
<p-button (click)="toggleMenu()" icon="pi pi-bars" outlined size="large" />
|
||||
<p-button (click)="refresh()" icon="pi pi-bars" outlined size="large" />
|
||||
<p-button (click)="doPay()" icon="pi pi-bars" outlined size="large" />
|
||||
</ng-template>
|
||||
|
||||
<ng-template #topbarCenter>
|
||||
@@ -20,7 +22,7 @@
|
||||
<ng-template #topbarEnd>
|
||||
<div class="">
|
||||
<p-menu #menu [model]="profileMenuItems" [popup]="true" />
|
||||
<p-button (click)="menu.toggle($event)" icon="pi pi-user" text severity="contrast" size="large" />
|
||||
<p-button (click)="menu.toggle($event)" icon="pi pi-user" text outlined size="large" />
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AuthService } from '@/core';
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { LayoutService } from '@/layout/service/layout.service';
|
||||
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
||||
import {
|
||||
@@ -36,6 +38,10 @@ import { PosMainMenuSidebarComponent } from './mainMenuSidebar/main-menu-sidebar
|
||||
})
|
||||
export class PosLayoutComponent implements AfterViewInit {
|
||||
constructor() {}
|
||||
|
||||
private readonly nativeBridgeService = inject(NativeBridgeService);
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
@ViewChild('topbarStart', { static: true }) topbarStart!: TemplateRef<any>;
|
||||
@ViewChild('topbarCenter', { static: true }) topbarCenter!: TemplateRef<any>;
|
||||
@ViewChild('topbarEnd', { static: true }) topbarEnd!: TemplateRef<any>;
|
||||
@@ -95,6 +101,13 @@ export class PosLayoutComponent implements AfterViewInit {
|
||||
this.getData();
|
||||
}
|
||||
|
||||
doPay() {
|
||||
this.nativeBridgeService.pay({
|
||||
amount: 1000,
|
||||
id: '1',
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.layoutService.changeIsFullPage(true);
|
||||
this.getData();
|
||||
@@ -107,8 +120,13 @@ export class PosLayoutComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.layoutService.changeIsFullPage(false);
|
||||
this.layoutService.setTopbarStartSlot(null);
|
||||
this.layoutService.setTopbarCenterSlot(null);
|
||||
this.layoutService.setTopbarEndSlot(null);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { InputComponent, KeyValueComponent } from '@/shared/components';
|
||||
@@ -37,6 +38,7 @@ export class PosPaymentFormDialogComponent
|
||||
{
|
||||
private readonly store = inject(PosLandingStore);
|
||||
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
|
||||
private readonly nativeBridgeService = inject(NativeBridgeService);
|
||||
private readonly toastServices = inject(ToastService);
|
||||
|
||||
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
|
||||
@@ -203,15 +205,6 @@ export class PosPaymentFormDialogComponent
|
||||
|
||||
override showSuccessMessage = false;
|
||||
|
||||
override ngOnInit() {
|
||||
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener((payload) => {
|
||||
const parsedAmount = this.extractAmountFromPaymentResult(payload);
|
||||
if (parsedAmount !== null) {
|
||||
this.payedInTerminal.update((items) => [...items, parsedAmount]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.restorePaymentCallback?.();
|
||||
}
|
||||
@@ -230,48 +223,70 @@ export class PosPaymentFormDialogComponent
|
||||
terminals: (rawPayment.terminals || []).map((value) => Number(value || 0)),
|
||||
};
|
||||
|
||||
if (payment.terminals.some((terminal) => terminal > 0) && this.paymentBridge.isEnabled()) {
|
||||
for (let terminal of payment.terminals) {
|
||||
this.toastServices.info({ text: 'در حال پردازش پرداخت با دستگاه...', life: 3000 });
|
||||
if (terminal <= 0) continue;
|
||||
this.toastServices.info({ text: ' دستگاه...', life: 3000 });
|
||||
// @ts-ignore
|
||||
window.AndroidBridge?.pay(terminal, '0');
|
||||
const payResult = this.paymentBridge.pay({
|
||||
amount: terminal,
|
||||
id: '0',
|
||||
});
|
||||
|
||||
if (!payResult.success) {
|
||||
return this.toastServices.warn({
|
||||
text: payResult.error || 'پرداخت با دستگاه انجام نشد. لطفا دوباره تلاش کنید.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.store.setPayment(payment);
|
||||
|
||||
this.store
|
||||
.submitOrder()
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
return throwError(() => err);
|
||||
}),
|
||||
)
|
||||
.subscribe((res) => {
|
||||
if (this.paymentBridge.isEnabled()) {
|
||||
this.paymentBridge.print({
|
||||
invoiceId: res?.id,
|
||||
code: res?.code,
|
||||
const terminalPaymentIsDone = this.checkTerminalPayments();
|
||||
|
||||
if (terminalPaymentIsDone) {
|
||||
this.store
|
||||
.submitOrder()
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
return throwError(() => err);
|
||||
}),
|
||||
)
|
||||
.subscribe((res) => {
|
||||
this.close();
|
||||
this.onSubmit.emit();
|
||||
this.toastServices.success({
|
||||
text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||
});
|
||||
}
|
||||
this.close();
|
||||
this.onSubmit.emit();
|
||||
this.toastServices.success({
|
||||
text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private checkTerminalPayments(): boolean {
|
||||
this.form.controls.terminals.controls = this.form.controls.terminals.controls.filter(
|
||||
(control) => control.value,
|
||||
);
|
||||
const terminalPayments = this.form.controls.terminals.controls.map(
|
||||
(terminal) => terminal.value!,
|
||||
);
|
||||
|
||||
this.selectedPayByTerminalStep.set(terminalPayments.length);
|
||||
|
||||
if (terminalPayments.length) {
|
||||
if (this.nativeBridgeService.isEnabled()) {
|
||||
for (let i in terminalPayments) {
|
||||
const terminal = terminalPayments[i];
|
||||
this.toastServices.info({ text: 'در حال پردازش پرداخت با دستگاه...', life: 3000 });
|
||||
this.payByTerminal(parseInt(i), terminal);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.toastServices.error({
|
||||
text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private payByTerminal(id: number, amount: number) {
|
||||
const payResult = this.nativeBridgeService.pay({
|
||||
amount,
|
||||
id: id.toString(),
|
||||
});
|
||||
|
||||
// if (!payResult.success) {
|
||||
// return this.toastServices.warn({
|
||||
// text: payResult.error || 'پرداخت با دستگاه انجام نشد. لطفا دوباره تلاش کنید.',
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
override onSuccess(response: IPayment): void {}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { INativeBridgeResult, INativePayRequest, INativePrintRequest } from '@/core/services/native-bridge.service';
|
||||
|
||||
export abstract class PosPaymentBridgeAbstract {
|
||||
abstract isEnabled(): boolean;
|
||||
abstract canPay(): boolean;
|
||||
abstract canPrint(): boolean;
|
||||
abstract pay(request: INativePayRequest): INativeBridgeResult;
|
||||
abstract print(request: INativePrintRequest): INativeBridgeResult;
|
||||
abstract registerPaymentResultListener(handler: (payload: unknown) => void): () => void;
|
||||
abstract emitPaymentResultForTest(payload: unknown): void;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,36 @@
|
||||
import { NativeBridgeService } from '@/core/services';
|
||||
import {
|
||||
INativeBridgeResult,
|
||||
INativePayRequest,
|
||||
INativePrintRequest,
|
||||
} from '@/core/services/native-bridge.service';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { PosPaymentBridgeAbstract } from './payment-bridge.abstract';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PosPaymentBridgeService extends PosPaymentBridgeAbstract {
|
||||
private readonly nativeBridge = inject(NativeBridgeService);
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.nativeBridge.isEnabled();
|
||||
}
|
||||
|
||||
canPay(): boolean {
|
||||
return this.nativeBridge.canPay();
|
||||
}
|
||||
|
||||
canPrint(): boolean {
|
||||
return this.nativeBridge.canPrint();
|
||||
}
|
||||
|
||||
pay(request: INativePayRequest): INativeBridgeResult {
|
||||
return this.nativeBridge.pay(request);
|
||||
}
|
||||
|
||||
print(request: INativePrintRequest): INativeBridgeResult {
|
||||
return this.nativeBridge.print(request);
|
||||
}
|
||||
|
||||
registerPaymentResultListener(handler: (payload: unknown) => void): () => void {
|
||||
const w = window as unknown as {
|
||||
onPaymentResult?: (payload: unknown) => void;
|
||||
AndroidBridge?: {
|
||||
WebV: {
|
||||
onPaymentResult?: (payload: unknown) => void;
|
||||
AndroidBridge?: {
|
||||
onPaymentResult?: (payload: unknown) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const previousGlobal = w.onPaymentResult;
|
||||
const previousBridge = w.AndroidBridge?.onPaymentResult;
|
||||
const previousGlobal = w.WebV.onPaymentResult;
|
||||
const previousBridge = w.WebV.AndroidBridge?.onPaymentResult;
|
||||
|
||||
w.onPaymentResult = handler;
|
||||
if (w.AndroidBridge) {
|
||||
w.AndroidBridge.onPaymentResult = handler;
|
||||
w.WebV.onPaymentResult = handler;
|
||||
if (w.WebV.AndroidBridge) {
|
||||
w.WebV.AndroidBridge.onPaymentResult = handler;
|
||||
}
|
||||
|
||||
return () => {
|
||||
w.onPaymentResult = previousGlobal;
|
||||
if (w.AndroidBridge) {
|
||||
w.AndroidBridge.onPaymentResult = previousBridge;
|
||||
w.WebV.onPaymentResult = previousGlobal;
|
||||
if (w.WebV.AndroidBridge) {
|
||||
w.WebV.AndroidBridge.onPaymentResult = previousBridge;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emitPaymentResultForTest(payload: unknown): void {
|
||||
const w = window as unknown as { onPaymentResult?: (payload: unknown) => void };
|
||||
w.onPaymentResult?.(payload);
|
||||
const w = window as unknown as { WebV: { onPaymentResult?: (payload: unknown) => void } };
|
||||
w.WebV.onPaymentResult?.(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { stockKeepingUnitsNamedRoutes } from '../modules/stockKeepingUnits/constants';
|
||||
export const SUPER_ADMIN_MENU_ITEMS = [
|
||||
{
|
||||
items: [
|
||||
@@ -9,19 +8,19 @@ export const SUPER_ADMIN_MENU_ITEMS = [
|
||||
routerLink: ['/'],
|
||||
},
|
||||
{
|
||||
label: 'مشتریها',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
label: 'مشتری',
|
||||
icon: 'pi pi-fw pi-id-card',
|
||||
routerLink: ['/super_admin/consumers'],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'شرکای تجاری',
|
||||
icon: 'pi pi-fw pi-credit-card',
|
||||
label: 'شریک تجاری',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
routerLink: ['/super_admin/partners'],
|
||||
},
|
||||
{
|
||||
label: 'ارایهدهندگان',
|
||||
icon: 'pi pi-fw pi-credit-card',
|
||||
label: 'ارایهدهنده',
|
||||
icon: 'pi pi-fw pi-building-columns',
|
||||
routerLink: ['/super_admin/providers'],
|
||||
},
|
||||
],
|
||||
@@ -33,27 +32,27 @@ export const SUPER_ADMIN_MENU_ITEMS = [
|
||||
items: [
|
||||
{
|
||||
label: 'اصناف',
|
||||
icon: 'pi pi-fw pi-building',
|
||||
icon: 'pi pi-fw pi-briefcase',
|
||||
routerLink: ['/super_admin/guilds'],
|
||||
},
|
||||
// {
|
||||
// label: 'شناسه کالا',
|
||||
// icon: 'pi pi-fw pi-barcode',
|
||||
// routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()],
|
||||
// },
|
||||
{
|
||||
label: 'شناسه کالاها',
|
||||
icon: 'pi pi-fw pi-good',
|
||||
routerLink: [stockKeepingUnitsNamedRoutes.stockKeepingUnits.meta.pagePath!()],
|
||||
},
|
||||
{
|
||||
label: 'لایسنسها',
|
||||
icon: 'pi pi-fw pi-credit-card',
|
||||
label: 'لایسنس',
|
||||
icon: 'pi pi-fw pi-key',
|
||||
routerLink: ['/super_admin/licenses'],
|
||||
},
|
||||
{
|
||||
label: 'برندهای دستگاه',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
label: 'برند دستگاه',
|
||||
icon: 'pi pi-fw pi-receipt',
|
||||
routerLink: ['/super_admin/device_brands'],
|
||||
},
|
||||
{
|
||||
label: 'دستگاهها',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
label: 'دستگاه',
|
||||
icon: 'pi pi-fw pi-tablet',
|
||||
routerLink: ['/super_admin/devices'],
|
||||
},
|
||||
],
|
||||
@@ -63,7 +62,7 @@ export const SUPER_ADMIN_MENU_ITEMS = [
|
||||
icon: 'pi pi-fw pi-cog',
|
||||
items: [
|
||||
{
|
||||
label: 'کاربران',
|
||||
label: 'حسابهای کاربری',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
routerLink: ['/super_admin/users'],
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export class ConsumerAccountListComponent extends AbstractList<IConsumerAccountR
|
||||
@Input({ required: true }) consumerId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'username',
|
||||
header: 'نام کاربری',
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export class ConsumerBusinessActivitiesComponent extends AbstractList<IBusinessA
|
||||
@Input({ required: true }) consumerId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'guild', header: 'صنف', type: 'nested', nestedOption: { path: 'guild.name' } },
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ export class ConsumerComplexesComponent extends AbstractList<IComplexResponse> {
|
||||
@Input() businessId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{
|
||||
field: 'created_at',
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ConsumerPosesComponent extends AbstractList<IPosResponse> {
|
||||
@Input({ required: true }) complexId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'serial_number', header: 'شماره سریال' },
|
||||
{ field: 'device', header: 'دستگاه', type: 'nested', nestedOption: { path: 'device.name' } },
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[items]="items()"
|
||||
[loading]="loading()"
|
||||
[showDetails]="true"
|
||||
[showEdit]="true"
|
||||
(onAdd)="openAddForm()"
|
||||
(onDetails)="toSinglePage($event)"
|
||||
(onEdit)="onEditClick($event)"
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ConsumersComponent extends AbstractList<IAdminConsumerResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'نام' },
|
||||
{ field: 'business_counts', header: 'تعداد فعالیتهای اقتصادی' },
|
||||
{
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
(onHide)="close()"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
<app-input label="عنوان" [control]="form.controls.name" name="name" />
|
||||
<field-name [control]="form.controls.name" />
|
||||
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||
</form>
|
||||
</shared-dialog>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
import { NameComponent } from '@/shared/components';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { fieldControl } from '@/shared/constants';
|
||||
import { Component, computed, inject, Input } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { IDeviceBrandRequest, IDeviceBrandResponse } from '../models';
|
||||
import { DeviceBrandsService } from '../services/main.service';
|
||||
|
||||
@Component({
|
||||
selector: 'deviceBrand-form',
|
||||
templateUrl: './form.component.html',
|
||||
imports: [ReactiveFormsModule, SharedDialogComponent, InputComponent, FormFooterActionsComponent],
|
||||
imports: [ReactiveFormsModule, SharedDialogComponent, FormFooterActionsComponent, NameComponent],
|
||||
})
|
||||
export class DeviceBrandFormComponent extends AbstractFormDialog<
|
||||
IDeviceBrandRequest,
|
||||
@@ -20,10 +21,7 @@ export class DeviceBrandFormComponent extends AbstractFormDialog<
|
||||
private service = inject(DeviceBrandsService);
|
||||
|
||||
form = this.fb.group({
|
||||
name: this.fb.control<string>(this.initialValues?.name || '', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
name: fieldControl.name(this.initialValues?.name),
|
||||
});
|
||||
|
||||
preparedTitle = computed(() => `${this.editMode ? 'ویرایش' : 'افزودن'} برند دستگاه`);
|
||||
|
||||
@@ -16,7 +16,7 @@ export class DeviceBrandsComponent extends AbstractList<IDeviceBrandResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{
|
||||
field: 'created_at',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(onHide)="close()"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
<app-input label="عنوان" [control]="form.controls.name" name="name" />
|
||||
<field-name [control]="form.controls.name" />
|
||||
<catalog-deviceBrand-select [control]="form.controls.brand_id" />
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// import { CatalogRolesComponent } from '@/shared/catalog/roles';
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { CatalogDeviceBrandSelectComponent } from '@/shared/catalog/deviceBrands';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
import { NameComponent } from '@/shared/components';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
@@ -15,9 +15,9 @@ import { SuperAdminDeviceService } from '../services/main.service';
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
SharedDialogComponent,
|
||||
InputComponent,
|
||||
FormFooterActionsComponent,
|
||||
CatalogDeviceBrandSelectComponent,
|
||||
NameComponent,
|
||||
],
|
||||
})
|
||||
export class UserComplexFormComponent extends AbstractFormDialog<IDeviceRequest, IDeviceResponse> {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class DevicesComponent extends AbstractList<IDeviceResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{
|
||||
field: 'brand',
|
||||
|
||||
@@ -18,7 +18,7 @@ export class GuildGoodCategoriesListComponent extends AbstractList<IGoodCategori
|
||||
@Input() guildId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'goods_count', header: 'تعداد کالاها' },
|
||||
{
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<shared-dialog
|
||||
header="فرم شناسه کالا"
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[style]="{ width: '500px' }"
|
||||
[closable]="true"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
<field-code [control]="form.controls.code" />
|
||||
<field-name [control]="form.controls.name" />
|
||||
<field-vat [control]="form.controls.VAT" />
|
||||
<field-sku-type [control]="form.controls.type" />
|
||||
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||
</form>
|
||||
</shared-dialog>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import {
|
||||
CodeComponent,
|
||||
NameComponent,
|
||||
SkuTypeComponent,
|
||||
VatComponent,
|
||||
} from '@/shared/components/fields';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { fieldControl } from '@/shared/constants/fields';
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { IStockKeepingUnitRequest, IStockKeepingUnitResponse } from '../../models';
|
||||
import { StockKeepingUnitsService } from '../../services/stockKeepingUnits.service';
|
||||
|
||||
@Component({
|
||||
selector: 'stock-keeping-unit-form',
|
||||
templateUrl: './form.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
SharedDialogComponent,
|
||||
NameComponent,
|
||||
CodeComponent,
|
||||
VatComponent,
|
||||
SkuTypeComponent,
|
||||
FormFooterActionsComponent,
|
||||
],
|
||||
})
|
||||
export class StockKeepingUnitFormComponent extends AbstractFormDialog<
|
||||
IStockKeepingUnitRequest,
|
||||
IStockKeepingUnitResponse
|
||||
> {
|
||||
@Input({ required: true }) guildId!: string;
|
||||
@Input() stockKeepingUnitId?: string;
|
||||
|
||||
private readonly service = inject(StockKeepingUnitsService);
|
||||
|
||||
form = this.fb.group({
|
||||
code: fieldControl.code(this.initialValues?.code || ''),
|
||||
name: fieldControl.name(this.initialValues?.name || ''),
|
||||
VAT: fieldControl.VAT(String(this.initialValues?.VAT ?? 0)),
|
||||
type: fieldControl.type(this.initialValues?.type || 'GOLD'),
|
||||
is_public: fieldControl.is_public(String(this.initialValues?.is_public ?? true), false),
|
||||
is_domestic: fieldControl.is_domestic(String(this.initialValues?.is_domestic ?? true), false),
|
||||
});
|
||||
|
||||
override ngOnChanges() {
|
||||
this.form.patchValue({
|
||||
code: this.initialValues?.code ?? '',
|
||||
name: this.initialValues?.name ?? '',
|
||||
VAT: String(this.initialValues?.VAT ?? 0),
|
||||
type: this.initialValues?.type ?? 'GOLD',
|
||||
is_public: String(this.initialValues?.is_public ?? true),
|
||||
is_domestic: String(this.initialValues?.is_domestic ?? true),
|
||||
});
|
||||
if (this.editMode && !this.stockKeepingUnitId) {
|
||||
throw 'missing some arguments';
|
||||
}
|
||||
}
|
||||
|
||||
override submitForm(payload: IStockKeepingUnitRequest) {
|
||||
const preparedPayload: IStockKeepingUnitRequest = {
|
||||
...payload,
|
||||
VAT: Number(payload.VAT),
|
||||
is_public: String(payload.is_public) === 'true',
|
||||
is_domestic: String(payload.is_domestic) === 'true',
|
||||
};
|
||||
if (this.editMode) {
|
||||
return this.service.update(this.guildId, this.stockKeepingUnitId!, preparedPayload);
|
||||
}
|
||||
return this.service.create(this.guildId, preparedPayload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const baseUrl = (guildId: string) => `/api/v1/admin/guilds/${guildId}/stock-keeping-units`;
|
||||
|
||||
export const GUILD_STOCK_KEEPING_UNITS_API_ROUTES = {
|
||||
list: (guildId: string) => `${baseUrl(guildId)}`,
|
||||
single: (guildId: string, id: string) => `${baseUrl(guildId)}/${id}`,
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { GUILD_GOOD_CATEGORIES_ROUTES } from './goodCategories';
|
||||
import { GUILD_GOODS_ROUTES } from './goods';
|
||||
import { GUILD_STOCK_KEEPING_UNITS_ROUTES } from './stockKeepingUnits';
|
||||
|
||||
export type TGuildsRouteNames = 'guilds' | 'guild';
|
||||
|
||||
@@ -40,6 +41,8 @@ export const GUILDS_ROUTES: Routes = [
|
||||
|
||||
...GUILD_GOOD_CATEGORIES_ROUTES,
|
||||
...GUILD_GOODS_ROUTES,
|
||||
...GUILD_STOCK_KEEPING_UNITS_ROUTES,
|
||||
|
||||
// {
|
||||
// path: 'good_categories/:categoryId',
|
||||
// loadComponent: () => import('../../views/categories/').then((m) => m.GuildComponent),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export type TStockKeepingUnitsRouteNames = 'stockKeepingUnits';
|
||||
|
||||
export const stockKeepingUnitsNamedRoutes: NamedRoutes<TStockKeepingUnitsRouteNames> = {
|
||||
stockKeepingUnits: {
|
||||
path: 'stock-keeping-units',
|
||||
loadComponent: () =>
|
||||
import('../../views/stockKeepingUnits/list.component').then(
|
||||
(m) => m.StockKeepingUnitsComponent,
|
||||
),
|
||||
meta: {
|
||||
title: 'شناسه کالاها',
|
||||
pagePath: () => '/super_admin/guilds/:guildId/stock-keeping-units',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GUILD_STOCK_KEEPING_UNITS_ROUTES: Routes = Object.values(stockKeepingUnitsNamedRoutes);
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './goods_io';
|
||||
export * from './io';
|
||||
export * from './stockKeepingUnits_io';
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface IGuildResponse extends IGuildRawResponse {}
|
||||
export interface IGuildRequest {
|
||||
name: string;
|
||||
code: string;
|
||||
invoice_template_type: string;
|
||||
}
|
||||
|
||||
export interface IGoodCategoriesRawResponse {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface IStockKeepingUnitRawResponse {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
VAT: number;
|
||||
type: string;
|
||||
is_public: boolean;
|
||||
is_domestic: boolean;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface IStockKeepingUnitResponse extends IStockKeepingUnitRawResponse {}
|
||||
|
||||
export interface IStockKeepingUnitRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
VAT: number;
|
||||
type?: string;
|
||||
is_public?: boolean;
|
||||
is_domestic?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IPaginatedResponse } from '@/core/models/service.model';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { GUILD_STOCK_KEEPING_UNITS_API_ROUTES } from '../constants/apiRoutes/stockKeepingUnits';
|
||||
import {
|
||||
IStockKeepingUnitRawResponse,
|
||||
IStockKeepingUnitRequest,
|
||||
IStockKeepingUnitResponse,
|
||||
} from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StockKeepingUnitsService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
private apiRoutes = GUILD_STOCK_KEEPING_UNITS_API_ROUTES;
|
||||
|
||||
getAll(guildId: string): Observable<IPaginatedResponse<IStockKeepingUnitResponse>> {
|
||||
return this.http.get<IPaginatedResponse<IStockKeepingUnitRawResponse>>(
|
||||
this.apiRoutes.list(guildId),
|
||||
);
|
||||
}
|
||||
|
||||
create(
|
||||
guildId: string,
|
||||
payload: IStockKeepingUnitRequest,
|
||||
): Observable<IStockKeepingUnitResponse> {
|
||||
return this.http.post<IStockKeepingUnitResponse>(this.apiRoutes.list(guildId), payload);
|
||||
}
|
||||
|
||||
update(
|
||||
guildId: string,
|
||||
id: string,
|
||||
payload: IStockKeepingUnitRequest,
|
||||
): Observable<IStockKeepingUnitResponse> {
|
||||
return this.http.patch<IStockKeepingUnitResponse>(this.apiRoutes.single(guildId, id), payload);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { GuildGoodCategoriesListComponent } from '../../components/categories/li
|
||||
imports: [GuildGoodCategoriesListComponent],
|
||||
})
|
||||
export class GuildGoodCategoriesComponent {
|
||||
route = inject(ActivatedRoute);
|
||||
guildId = signal<string>(this.route.snapshot.paramMap.get('guildId')!);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
readonly guildId = signal<string>(this.route.snapshot.paramMap.get('guildId')!);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class GuildsComponent extends AbstractList<IGuildResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'code', header: 'کد' },
|
||||
{ field: 'businessActivitiesCount', header: 'تعداد فعالیتهای اقتصادی مرتبط' },
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<app-page-data-list
|
||||
[pageTitle]="'مدیریت شناسه کالاها'"
|
||||
[addNewCtaLabel]="'افزودن شناسه کالا'"
|
||||
[columns]="columns"
|
||||
emptyPlaceholderTitle="شناسه کالایی یافت نشد"
|
||||
emptyPlaceholderDescription="برای افزودن شناسه کالا، روی دکمهٔ بالا کلیک کنید."
|
||||
[items]="items()"
|
||||
[loading]="loading()"
|
||||
[showDetails]="false"
|
||||
[showAdd]="true"
|
||||
(onAdd)="openAddForm()"
|
||||
(onRefresh)="refresh()"
|
||||
>
|
||||
</app-page-data-list>
|
||||
<stock-keeping-unit-form
|
||||
[guildId]="guildId()"
|
||||
[(visible)]="visibleForm"
|
||||
[initialValues]="selectedItemForEdit()!"
|
||||
[editMode]="false"
|
||||
[stockKeepingUnitId]="selectedItemForEdit()?.id"
|
||||
(onSubmit)="refresh()"
|
||||
/>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AbstractList } from '@/shared/abstractClasses/abstract-list';
|
||||
import { PageDataListComponent } from '@/shared/components/pageDataList/page-data-list.component';
|
||||
import pageParamsUtils from '@/utils/page-params.utils';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { StockKeepingUnitFormComponent } from '../../components/stockKeepingUnits/form.component';
|
||||
import { IStockKeepingUnitResponse } from '../../models';
|
||||
import { StockKeepingUnitsService } from '../../services/stockKeepingUnits.service';
|
||||
|
||||
@Component({
|
||||
selector: 'superAdmin-stockKeepingUnits',
|
||||
templateUrl: './list.component.html',
|
||||
imports: [PageDataListComponent, StockKeepingUnitFormComponent],
|
||||
})
|
||||
export class StockKeepingUnitsComponent extends AbstractList<IStockKeepingUnitResponse> {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
pageParams = computed(() => pageParamsUtils(this.route));
|
||||
readonly guildId = signal<string>(this.pageParams()['guildId']!);
|
||||
|
||||
private readonly service = inject(StockKeepingUnitsService);
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'code', header: 'کد' },
|
||||
{ field: 'name', header: 'عنوان' },
|
||||
{ field: 'VAT', header: 'مالیات ارزش افزوده' },
|
||||
];
|
||||
}
|
||||
|
||||
override getDataRequest() {
|
||||
return this.service.getAll(this.guildId());
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export class ConsumerAccountListComponent extends AbstractList<IPartnerAccountRe
|
||||
@Input({ required: true }) partnerId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'username',
|
||||
header: 'نام کاربری',
|
||||
|
||||
@@ -17,7 +17,7 @@ export class AdminPartnerLicensesComponent extends AbstractList<IPartnerActivate
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{
|
||||
field: 'consumer',
|
||||
header: 'مشتری',
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ProvidersComponent extends AbstractList<IProviderResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
// { field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'name', header: 'نام' },
|
||||
{
|
||||
field: 'created_at',
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<shared-dialog header="فرم واحد شمارش کالا" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
|
||||
<shared-dialog
|
||||
header="فرم شناسه کالا"
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[style]="{ width: '500px' }"
|
||||
[closable]="true"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
<field-code [control]="form.controls.code" />
|
||||
<field-name [control]="form.controls.name" />
|
||||
<field-vat [control]="$any(form.controls.VAT)" />
|
||||
<field-vat [control]="form.controls.VAT" />
|
||||
<field-sku-type [control]="form.controls.type" />
|
||||
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||
import { fieldControl } from '@/shared/constants/fields';
|
||||
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
|
||||
import {
|
||||
CodeComponent,
|
||||
NameComponent,
|
||||
SkuTypeComponent,
|
||||
VatComponent,
|
||||
} from '@/shared/components/fields';
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { CodeComponent, NameComponent, SkuTypeComponent, VatComponent } from '@/shared/components/fields';
|
||||
import { fieldControl } from '@/shared/constants/fields';
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { IStockKeepingUnitRequest, IStockKeepingUnitResponse } from '../models';
|
||||
|
||||
@@ -9,7 +9,7 @@ export const stockKeepingUnitsNamedRoutes: NamedRoutes<TStockKeepingUnitsRouteNa
|
||||
loadComponent: () =>
|
||||
import('../../views/list.component').then((m) => m.StockKeepingUnitsComponent),
|
||||
meta: {
|
||||
title: 'واحدهای شمارش کالا',
|
||||
title: 'واحدهای شناسه کالا',
|
||||
pagePath: () => '/super_admin/stock-keeping-units',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
[pageTitle]="'مدیریت شناسه کالاها'"
|
||||
[addNewCtaLabel]="'افزودن شناسه کالا'"
|
||||
[columns]="columns"
|
||||
[showAdd]="true"
|
||||
emptyPlaceholderTitle="شناسه کالایی یافت نشد"
|
||||
emptyPlaceholderDescription="برای افزودن شناسه کالا، روی دکمهٔ بالا کلیک کنید."
|
||||
[items]="items()"
|
||||
@@ -12,13 +11,11 @@
|
||||
(onAdd)="openAddForm()"
|
||||
(onRefresh)="refresh()"
|
||||
>
|
||||
@if (selectedItemForEdit()) {
|
||||
<stock-keeping-unit-form
|
||||
[(visible)]="visibleForm"
|
||||
[initialValues]="selectedItemForEdit()!"
|
||||
[editMode]="editMode()"
|
||||
[stockKeepingUnitId]="selectedItemForEdit()?.id"
|
||||
(onSubmit)="refresh()"
|
||||
/>
|
||||
}
|
||||
</app-page-data-list>
|
||||
<stock-keeping-unit-form
|
||||
[(visible)]="visibleForm"
|
||||
[initialValues]="selectedItemForEdit()!"
|
||||
[editMode]="false"
|
||||
[stockKeepingUnitId]="selectedItemForEdit()?.id"
|
||||
(onSubmit)="refresh()"
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,6 @@ export class UsersComponent extends AbstractList<IAccountResponse> {
|
||||
@Input() userId!: string;
|
||||
@Input() fullHeight?: boolean;
|
||||
@Input() header: IColumn[] = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'username', header: 'نام کاربری' },
|
||||
{ field: 'type', header: 'نوع حساب' },
|
||||
{
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
[pageTitle]="'مدیریت کاربران'"
|
||||
[addNewCtaLabel]="'افزودن کاربر'"
|
||||
[columns]="columns"
|
||||
[showAdd]="true"
|
||||
emptyPlaceholderTitle="کاربری یافت نشد"
|
||||
emptyPlaceholderDescription="برای افزودن کاربر، روی دکمهٔ بالا کلیک کنید."
|
||||
[items]="items()"
|
||||
[loading]="loading()"
|
||||
[showDetails]="true"
|
||||
[showIndex]="false"
|
||||
[showAdd]="true"
|
||||
[showEdit]="true"
|
||||
(onAdd)="openAddForm()"
|
||||
(onEdit)="onEditClick($event)"
|
||||
(onDetails)="toSinglePage($event)"
|
||||
(onRefresh)="refresh()"
|
||||
/>
|
||||
<superAdmin-user-form
|
||||
|
||||
@@ -19,7 +19,6 @@ export class UsersComponent extends AbstractList<IUserResponse> {
|
||||
|
||||
override setColumns(): void {
|
||||
this.columns = [
|
||||
{ field: 'id', header: 'شناسه', type: 'id' },
|
||||
{ field: 'fullname', header: 'نام' },
|
||||
{ field: 'mobile_number', header: 'شماره موبایل' },
|
||||
{ field: 'national_code', header: 'کد ملی' },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, inject, Input, TemplateRef } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { Button, ButtonIcon } from 'primeng/button';
|
||||
import { Button } from 'primeng/button';
|
||||
import { MenuModule } from 'primeng/menu';
|
||||
import { StyleClassModule } from 'primeng/styleclass';
|
||||
import { Toolbar } from 'primeng/toolbar';
|
||||
@@ -13,8 +13,11 @@ import { LayoutService } from '../service/layout.service';
|
||||
@Component({
|
||||
selector: 'app-topbar',
|
||||
standalone: true,
|
||||
imports: [RouterModule, CommonModule, StyleClassModule, MenuModule, Button, Toolbar, ButtonIcon],
|
||||
imports: [RouterModule, CommonModule, StyleClassModule, MenuModule, Button, Toolbar],
|
||||
templateUrl: './app.topbar.component.html',
|
||||
host: {
|
||||
class: 'sticky top-0 z-50',
|
||||
},
|
||||
})
|
||||
export class AppTopbar {
|
||||
@Input() showMenu: boolean = false;
|
||||
|
||||
@@ -4,7 +4,12 @@ import { InputComponent } from '../input/input.component';
|
||||
|
||||
@Component({
|
||||
selector: 'field-vat',
|
||||
template: `<app-input label="مالیات بر ارزش افزوده" [control]="control" [name]="name" type="price" />`,
|
||||
template: `<app-input
|
||||
label="مالیات بر ارزش افزوده"
|
||||
[control]="control"
|
||||
[name]="name"
|
||||
type="number"
|
||||
/>`,
|
||||
imports: [ReactiveFormsModule, InputComponent],
|
||||
})
|
||||
export class VatComponent {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './form.component';
|
||||
export * from './types';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export class InputComponent {
|
||||
@Input() maxLength?: number;
|
||||
@Input() min?: number;
|
||||
@Input() max?: number;
|
||||
@Input() fixed?: number;
|
||||
@Output() valueChange = new EventEmitter<string | number>();
|
||||
@Output() blur = new EventEmitter<void>();
|
||||
|
||||
@@ -111,12 +112,13 @@ export class InputComponent {
|
||||
|
||||
get inputMode(): string | null {
|
||||
switch (this.type) {
|
||||
case 'price':
|
||||
return 'decimal';
|
||||
case 'number':
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
case 'price':
|
||||
case 'number':
|
||||
return 'numeric';
|
||||
default:
|
||||
return 'text';
|
||||
@@ -169,11 +171,12 @@ export class InputComponent {
|
||||
return 'email';
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'price':
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'price':
|
||||
case 'number':
|
||||
return 'text';
|
||||
case 'checkbox':
|
||||
return 'checkbox';
|
||||
case 'switch':
|
||||
@@ -190,59 +193,98 @@ export class InputComponent {
|
||||
onPriceInput($event: InputNumberInputEvent) {
|
||||
this.onInput($event.originalEvent, true);
|
||||
}
|
||||
|
||||
private toEnglishDigits(value: string): string {
|
||||
return value
|
||||
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
|
||||
.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632));
|
||||
}
|
||||
|
||||
private sanitizeNumericValue(value: string, allowDecimal: boolean): string {
|
||||
const normalized = this.toEnglishDigits(value).replace(/,/g, '');
|
||||
const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '');
|
||||
if (!allowDecimal) return cleaned;
|
||||
|
||||
const [integerPart = '', ...decimalParts] = cleaned.split('.');
|
||||
const mergedDecimals = decimalParts.join('');
|
||||
return decimalParts.length ? `${integerPart}.${mergedDecimals}` : integerPart;
|
||||
}
|
||||
|
||||
private applyFixed(value: string): string {
|
||||
if (this.fixed === undefined || this.fixed === null || this.fixed < 0 || value === '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.fixed === 0) {
|
||||
return value.split('.')[0] || '0';
|
||||
}
|
||||
|
||||
const [integerPart = '0', decimalPart = ''] = value.split('.');
|
||||
const normalizedInteger = integerPart === '' ? '0' : integerPart;
|
||||
const fixedDecimal = decimalPart.slice(0, this.fixed).padEnd(this.fixed, '0');
|
||||
return `${normalizedInteger}.${fixedDecimal}`;
|
||||
}
|
||||
|
||||
onInput($event: Event, isPriceFormat?: boolean) {
|
||||
// @ts-ignore
|
||||
let value = $event.target.value as string;
|
||||
// @ts-ignore
|
||||
const isDotInput = $event.data === '.';
|
||||
const target = $event.target as HTMLInputElement | null;
|
||||
if (!target) return;
|
||||
|
||||
if (isPriceFormat) {
|
||||
value = value.replace(/,/g, '');
|
||||
let value = target.value ?? '';
|
||||
const allowDecimal = this.type === 'price' || this.type === 'number';
|
||||
|
||||
if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) {
|
||||
value = this.sanitizeNumericValue(value, allowDecimal);
|
||||
value = this.applyFixed(value);
|
||||
}
|
||||
|
||||
let replacedTargetValue: Maybe<number | string> = null;
|
||||
if (this.inputMode === 'numeric' && !isDotInput) {
|
||||
let newValue = parseFloat(value || '0');
|
||||
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
||||
if (minValidator || maxValidator) {
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
console.log(value);
|
||||
|
||||
newValue = parseFloat(value.slice(0, value.length - 1));
|
||||
if (newValue < 0 || isNaN(newValue)) {
|
||||
newValue = 0;
|
||||
}
|
||||
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
|
||||
value = value.slice(0, this.preparedMaxLength);
|
||||
}
|
||||
|
||||
replacedTargetValue = isPriceFormat ? newValue.toLocaleString('en-US') : newValue;
|
||||
this.control.setValue(newValue);
|
||||
let numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
const minValidator = (this.min || this.min === 0) && numericValue < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && numericValue > this.max!;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
value !== '' &&
|
||||
(minValidator || maxValidator)
|
||||
) {
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (!['nationalId', 'phone', 'postalCode', 'mobile'].includes(this.type)) {
|
||||
this.control.setValue(newValue);
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.preparedMaxLength && this.preparedMaxLength < value.length) {
|
||||
let newValue = value.slice(0, value.length - 1);
|
||||
// @ts-ignore
|
||||
replacedTargetValue = newValue;
|
||||
this.control.setValue(newValue);
|
||||
value = value.slice(0, -1);
|
||||
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
}
|
||||
|
||||
if (replacedTargetValue !== null) {
|
||||
console.log(replacedTargetValue);
|
||||
const isIdentifierField = [
|
||||
'nationalId',
|
||||
'phone',
|
||||
'postalCode',
|
||||
'mobile',
|
||||
'email',
|
||||
'password',
|
||||
'simple',
|
||||
].includes(this.type);
|
||||
this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue);
|
||||
|
||||
// @ts-ignore
|
||||
$event.value = replacedTargetValue;
|
||||
// @ts-ignore
|
||||
$event.target.value = replacedTargetValue;
|
||||
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
|
||||
console.log('viewValue', viewValue);
|
||||
|
||||
target.value = viewValue;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
this.valueChange.observed
|
||||
) {
|
||||
this.valueChange.emit(isIdentifierField ? value : numericValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
}"
|
||||
>
|
||||
<ng-content>
|
||||
@if (valueType() === "badge") {
|
||||
<p-badge [value]="valueToShow()?.toString()" [severity]="value ? 'success' : 'danger'" />
|
||||
@if (valueType() === "tag") {
|
||||
<p-tag [value]="valueToShow()?.toString()" [severity]="value ? 'contrast' : 'danger'" />
|
||||
} @else {
|
||||
<span class="text-text-color text-base font-bold grow"> {{ valueToShow() }}</span>
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { formatDurationToText, formatJalali, JALALI_DATE_FORMATS } from '@/utils
|
||||
import priceMaskUtils from '@/utils/price-mask.utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, Input, signal } from '@angular/core';
|
||||
import { Badge } from 'primeng/badge';
|
||||
import { Tag } from 'primeng/tag';
|
||||
import { TDataType } from '../pageDataList/page-data-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-key-value',
|
||||
standalone: true,
|
||||
imports: [CommonModule, Badge],
|
||||
imports: [CommonModule, Tag],
|
||||
templateUrl: './key-value.component.html',
|
||||
host: {
|
||||
class: 'w-full block',
|
||||
@@ -18,24 +19,12 @@ export class KeyValueComponent {
|
||||
@Input() value?: string | boolean | number;
|
||||
@Input() hint?: string;
|
||||
@Input() alignment?: 'start' | 'center' | 'end' = 'start';
|
||||
@Input() type:
|
||||
| 'price'
|
||||
| 'active'
|
||||
| 'boolean'
|
||||
| 'has'
|
||||
| 'date'
|
||||
| 'dateTime'
|
||||
| 'duration'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'number'
|
||||
| 'simple' = 'simple';
|
||||
@Input() type: TDataType = 'simple';
|
||||
|
||||
@Input() trueValueToShow?: string;
|
||||
@Input() falseValueToShow?: string;
|
||||
@Input() variant?: 'badge' | 'text';
|
||||
@Input() variant?: 'tag' | 'text';
|
||||
tagSeverity = computed(() => (this.value ? 'success' : 'danger'));
|
||||
|
||||
valueToShow = signal(this.setValueToShow());
|
||||
|
||||
@@ -105,6 +94,6 @@ export class KeyValueComponent {
|
||||
}
|
||||
|
||||
valueType = computed(() =>
|
||||
this.variant || ['boolean', 'has', 'active'].includes(this.type) ? 'badge' : 'text',
|
||||
this.variant || ['boolean', 'has', 'active'].includes(this.type) ? 'tag' : 'text',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="listKeyValue">
|
||||
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
|
||||
@if (col.type !== "index") {
|
||||
<app-key-value [label]="col.header">
|
||||
<app-key-value [label]="col.header" [variant]="col.variant">
|
||||
@if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
@@ -27,6 +27,8 @@
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (col.variant === "tag") {
|
||||
<p-tag [value]="getCell(item, col)" [severity]="col.tagOptions?.severity || 'contrast'"></p-tag>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
|
||||
@@ -17,42 +17,9 @@ import { DrawerModule } from 'primeng/drawer';
|
||||
import { PaginatorModule } from 'primeng/paginator';
|
||||
import { SkeletonModule } from 'primeng/skeleton';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { Tag } from 'primeng/tag';
|
||||
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
||||
|
||||
type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: TDataType;
|
||||
|
||||
nestedOption?: {
|
||||
path: string;
|
||||
type?: TDataType;
|
||||
};
|
||||
thumbnailOptions?: {
|
||||
editable: boolean;
|
||||
deletable: boolean;
|
||||
showPreview: boolean;
|
||||
};
|
||||
dateOption?: {
|
||||
expiredMode?: boolean;
|
||||
dateTime?: boolean;
|
||||
onlyTime?: boolean;
|
||||
};
|
||||
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||
}
|
||||
import { IColumn } from './page-data-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-data-list-grid-view',
|
||||
@@ -69,6 +36,7 @@ export interface IColumn<T = any> {
|
||||
DrawerModule,
|
||||
UikitCopyComponent,
|
||||
KeyValueComponent,
|
||||
Tag,
|
||||
],
|
||||
})
|
||||
export class AppPageDataListGridView<I = any> {
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
|
||||
<tr>
|
||||
<td>
|
||||
@if (showIndex) {
|
||||
@if (showIndex) {
|
||||
<td>
|
||||
{{ i * (currentPage || 1) + 1 }}
|
||||
}
|
||||
</td>
|
||||
</td>
|
||||
}
|
||||
@for (col of columns; track col.field) {
|
||||
<td>
|
||||
@if (col.type === "thumbnail") {
|
||||
@@ -59,6 +59,8 @@
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (col.variant === "tag") {
|
||||
<p-tag [value]="getCell(item, col)" [severity]="col.tagOptions?.severity || 'contrast'"></p-tag>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
@@ -190,6 +192,7 @@
|
||||
[label]="col.header"
|
||||
[value]="item[expandableItemKey][col.field]"
|
||||
[type]="col.type || 'simple'"
|
||||
[variant]="col.variant"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -211,6 +214,11 @@
|
||||
<ng-template #loadingbody let-columns>
|
||||
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
|
||||
<tr style="height: 46px">
|
||||
@if (showIndex) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
</td>
|
||||
}
|
||||
@for (col of columns; track col) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
|
||||
@@ -17,43 +17,10 @@ import { DrawerModule } from 'primeng/drawer';
|
||||
import { PaginatorModule } from 'primeng/paginator';
|
||||
import { SkeletonModule } from 'primeng/skeleton';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { Tag } from 'primeng/tag';
|
||||
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
||||
import { TableActionRowComponent } from '../table-action-row.component';
|
||||
|
||||
type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: TDataType;
|
||||
|
||||
nestedOption?: {
|
||||
path: string;
|
||||
type?: TDataType;
|
||||
};
|
||||
thumbnailOptions?: {
|
||||
editable: boolean;
|
||||
deletable: boolean;
|
||||
showPreview: boolean;
|
||||
};
|
||||
dateOption?: {
|
||||
expiredMode?: boolean;
|
||||
dateTime?: boolean;
|
||||
onlyTime?: boolean;
|
||||
};
|
||||
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||
}
|
||||
import { IColumn } from './page-data-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-data-list-table-view',
|
||||
@@ -72,6 +39,7 @@ export interface IColumn<T = any> {
|
||||
DrawerModule,
|
||||
UikitCopyComponent,
|
||||
KeyValueComponent,
|
||||
Tag,
|
||||
],
|
||||
})
|
||||
export class AppPageDataListTableView<I = any> {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[showEdit]="showEdit"
|
||||
[showDelete]="showDelete"
|
||||
[showDetails]="showDetails"
|
||||
[showIndex]="showIndex"
|
||||
(onEdit)="edit($event)"
|
||||
(onDelete)="remove($event)"
|
||||
(onDetails)="details($event)"
|
||||
|
||||
@@ -22,7 +22,7 @@ import { TableModule } from 'primeng/table';
|
||||
import { AppPageDataListGridView } from './page-data-list-grid-view.component';
|
||||
import { AppPageDataListTableView } from './page-data-list-table-view.component';
|
||||
|
||||
type TDataType =
|
||||
export type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
@@ -31,7 +31,13 @@ type TDataType =
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
| 'badge'
|
||||
| 'number'
|
||||
| 'dateTime'
|
||||
| 'duration'
|
||||
| 'active'
|
||||
| 'has';
|
||||
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
@@ -39,6 +45,11 @@ export interface IColumn<T = any> {
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: TDataType;
|
||||
variant?: 'text' | 'tag';
|
||||
|
||||
tagOptions?: {
|
||||
severity?: 'success' | 'secondary' | 'info' | 'warn' | 'danger' | 'contrast';
|
||||
};
|
||||
|
||||
nestedOption?: {
|
||||
path: string;
|
||||
@@ -105,6 +116,7 @@ export class PageDataListComponent<I = any> {
|
||||
@Input() expandableItemKey?: string = '';
|
||||
@Input() expandColumns?: Maybe<IColumn[]> = null;
|
||||
@Input() dataKey?: string = 'items';
|
||||
@Input() showIndex?: boolean = true;
|
||||
|
||||
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
|
||||
@ContentChild('caption', { static: true }) caption!: TemplateRef<any> | null;
|
||||
|
||||
@@ -169,4 +169,9 @@ export const fieldControl = {
|
||||
value,
|
||||
isRequired ? required() : [],
|
||||
],
|
||||
|
||||
measure_unit: (value = '', isRequired = true): ControlConfig => [
|
||||
value,
|
||||
isRequired ? required() : [],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,9 +8,9 @@ export const goldKarat = {
|
||||
export type TGoldKarat = (typeof goldKarat)[keyof typeof goldKarat];
|
||||
|
||||
const translates: Record<string, string> = {
|
||||
KARAT_18: '18',
|
||||
KARAT_21: '21',
|
||||
KARAT_24: '24',
|
||||
KARAT_18: 'عیار ۱۸',
|
||||
KARAT_21: 'عیار ۲۱',
|
||||
KARAT_24: 'عیار ۲۴',
|
||||
};
|
||||
|
||||
export const goldKaratSelect: ISelectItem[] = Object.keys(goldKarat).map((key) => ({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.layout-sidebar {
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
height: calc(100vh - 8rem);
|
||||
height: calc(100vh - 7rem);
|
||||
z-index: 999;
|
||||
overflow-y: auto;
|
||||
user-select: none;
|
||||
|
||||
Reference in New Issue
Block a user