diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..37e9918 --- /dev/null +++ b/AGENT.md @@ -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-` + - template: `` + - inputs: `control` (required), optional `name`, optional `label` + +Example pattern: + +```ts +@Component({ + selector: 'field-example', + template: ``, + imports: [ReactiveFormsModule, InputComponent], +}) +export class ExampleComponent { + @Input({ required: true }) control = new FormControl(''); + @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.(initialValue, isRequired)` for consistency. +- In templates, render matching wrapper component and pass the matching control: + - `` +- 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. diff --git a/src/app/core/services/native-bridge.service.ts b/src/app/core/services/native-bridge.service.ts index 0c251b0..ce66162 100644 --- a/src/app/core/services/native-bridge.service.ts +++ b/src/app/core/services/native-bridge.service.ts @@ -4,9 +4,10 @@ import { Maybe } from '../models'; import { ToastService } from './toast.service'; interface INativeBridgeHost { - pay?: ((payload: string) => unknown) | ((amount: number, id: Maybe) => unknown); - print?: (payload: string) => unknown; + pay?: (amount: number, id: Maybe) => unknown; + print?: (payload: INativePrintRequest) => unknown; isEnabled?: () => boolean; + getUUID?: () => Maybe; } 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 { @@ -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) => 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 { + return typeof this.host?.getUUID === 'function' ? this.host.getUUID!() : null; + } + private tryParse(value: unknown): unknown { if (typeof value !== 'string') return value; try { diff --git a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts index 6dfa1b2..e776c3a 100644 --- a/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts +++ b/src/app/domains/consumer/components/invoices/shared-saleInvoice.component.ts @@ -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; + @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; } diff --git a/src/app/domains/consumer/constants/components/pos/pos.ts b/src/app/domains/consumer/constants/components/pos/pos.ts index db85e94..9d911a7 100644 --- a/src/app/domains/consumer/constants/components/pos/pos.ts +++ b/src/app/domains/consumer/constants/components/pos/pos.ts @@ -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: 'نوع دستگاه' }, diff --git a/src/app/domains/consumer/modules/accounts/views/list.component.ts b/src/app/domains/consumer/modules/accounts/views/list.component.ts index df65209..afd4804 100644 --- a/src/app/domains/consumer/modules/accounts/views/list.component.ts +++ b/src/app/domains/consumer/modules/accounts/views/list.component.ts @@ -24,7 +24,7 @@ export class ConsumerAccountsComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'username', header: 'نام کاربری', diff --git a/src/app/domains/consumer/modules/businessActivities/components/complexes/list.component.ts b/src/app/domains/consumer/modules/businessActivities/components/complexes/list.component.ts index f50dca0..c03d363 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/complexes/list.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/components/complexes/list.component.ts @@ -20,7 +20,7 @@ export class ConsumerComplexListComponent extends AbstractList @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: 'کد شعبه' }, { diff --git a/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.html b/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.html index 9ab4c83..e2cd491 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.html +++ b/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.html @@ -1,17 +1,11 @@ - -
- - - - - - - - - -
+ diff --git a/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.ts b/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.ts index 93ea10c..8f97191 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/components/goods/form.component.ts @@ -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(); 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); + }; } diff --git a/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts b/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts index e1da83c..327b81d 100644 --- a/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/components/goods/list.component.ts @@ -19,7 +19,7 @@ export class ConsumerBusinessActivityGoodsListComponent extends AbstractList { + create(businessId: string, data: FormData): Observable { return this.http.post( this.apiRoutes.list(businessId), data, @@ -41,7 +37,7 @@ export class BusinessActivityGoodsService { update( businessId: string, id: string, - data: IConsumerBusinessActivityGoodRequest, + data: FormData, ): Observable { return this.http.patch( this.apiRoutes.single(businessId, id), diff --git a/src/app/domains/consumer/modules/businessActivities/views/list.component.ts b/src/app/domains/consumer/modules/businessActivities/views/list.component.ts index a922e9b..00a2acc 100644 --- a/src/app/domains/consumer/modules/businessActivities/views/list.component.ts +++ b/src/app/domains/consumer/modules/businessActivities/views/list.component.ts @@ -15,7 +15,7 @@ export class ConsumerBusinessActivitiesComponent extends AbstractList { @Input() fullHeight?: boolean; @Input() header: IColumn[] = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'type', header: 'نوع مشتری', diff --git a/src/app/domains/consumer/modules/poses/store/pos.store.ts b/src/app/domains/consumer/modules/poses/store/pos.store.ts index 50a3efb..2ed87c0 100644 --- a/src/app/domains/consumer/modules/poses/store/pos.store.ts +++ b/src/app/domains/consumer/modules/poses/store/pos.store.ts @@ -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 { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'username', header: 'نام کاربری', diff --git a/src/app/domains/partner/modules/consumers/components/accounts/list.component.html b/src/app/domains/partner/modules/consumers/components/accounts/list.component.html index 7a1070f..d450361 100644 --- a/src/app/domains/partner/modules/consumers/components/accounts/list.component.html +++ b/src/app/domains/partner/modules/consumers/components/accounts/list.component.html @@ -3,6 +3,7 @@ [columns]="columns" emptyPlaceholderTitle="حساب کاربری‌ای یافت نشد." [items]="items()" + [showIndex]="false" [loading]="loading()" (onRefresh)="refresh()" > diff --git a/src/app/domains/partner/modules/consumers/components/accounts/list.component.ts b/src/app/domains/partner/modules/consumers/components/accounts/list.component.ts index 15e8a08..f7c05da 100644 --- a/src/app/domains/partner/modules/consumers/components/accounts/list.component.ts +++ b/src/app/domains/partner/modules/consumers/components/accounts/list.component.ts @@ -18,7 +18,6 @@ export class ConsumerAccountListComponent extends AbstractList diff --git a/src/app/domains/partner/modules/consumers/components/businessActivities/form-dialog.component.html b/src/app/domains/partner/modules/consumers/components/businessActivities/form-dialog.component.html index e5edd4e..35728d7 100644 --- a/src/app/domains/partner/modules/consumers/components/businessActivities/form-dialog.component.html +++ b/src/app/domains/partner/modules/consumers/components/businessActivities/form-dialog.component.html @@ -11,6 +11,7 @@ [businessActivityId]="businessActivityId" [initialValues]="initialValues" [editMode]="editMode" + [visible]="visible" (onSubmit)="onFormSubmit($event)" (onClose)="close()" /> diff --git a/src/app/domains/partner/modules/consumers/components/businessActivities/form.component.ts b/src/app/domains/partner/modules/consumers/components/businessActivities/form.component.ts index 03b6dba..28bc90d 100644 --- a/src/app/domains/partner/modules/consumers/components/businessActivities/form.component.ts +++ b/src/app/domains/partner/modules/consumers/components/businessActivities/form.component.ts @@ -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({ diff --git a/src/app/domains/partner/modules/consumers/components/complexes/list.component.ts b/src/app/domains/partner/modules/consumers/components/complexes/list.component.ts index a05e66d..dcf3075 100644 --- a/src/app/domains/partner/modules/consumers/components/complexes/list.component.ts +++ b/src/app/domains/partner/modules/consumers/components/complexes/list.component.ts @@ -26,7 +26,7 @@ export class ConsumerComplexesComponent extends AbstractList { @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: 'تعداد پایانه‌ها' }, diff --git a/src/app/domains/partner/modules/consumers/components/poses/list.component.ts b/src/app/domains/partner/modules/consumers/components/poses/list.component.ts index 13d90de..d98d037 100644 --- a/src/app/domains/partner/modules/consumers/components/poses/list.component.ts +++ b/src/app/domains/partner/modules/consumers/components/poses/list.component.ts @@ -22,7 +22,7 @@ export class ConsumerPosesComponent extends AbstractList { @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' } }, diff --git a/src/app/domains/partner/modules/consumers/views/single.component.html b/src/app/domains/partner/modules/consumers/views/single.component.html index 9011d07..c2ef4e8 100644 --- a/src/app/domains/partner/modules/consumers/views/single.component.html +++ b/src/app/domains/partner/modules/consumers/views/single.component.html @@ -11,7 +11,7 @@ } - + diff --git a/src/app/domains/partner/modules/customers/components/list.component.ts b/src/app/domains/partner/modules/customers/components/list.component.ts index 855b9e5..64da073 100644 --- a/src/app/domains/partner/modules/customers/components/list.component.ts +++ b/src/app/domains/partner/modules/customers/components/list.component.ts @@ -18,7 +18,7 @@ import { CustomersService } from '../services/main.service'; export class PartnerCustomerListComponent extends AbstractList { @Input() fullHeight?: boolean; @Input() header: IColumn[] = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'first_name', diff --git a/src/app/domains/partner/modules/licenses/components/list.component.ts b/src/app/domains/partner/modules/licenses/components/list.component.ts index 175a417..ec98202 100644 --- a/src/app/domains/partner/modules/licenses/components/list.component.ts +++ b/src/app/domains/partner/modules/licenses/components/list.component.ts @@ -15,7 +15,7 @@ export class PartnerLicenseListComponent extends AbstractList override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'business_activity', header: 'عنوان مشتری', diff --git a/src/app/domains/pos/layouts/layout.component.html b/src/app/domains/pos/layouts/layout.component.html index 4b65ce8..671c9f7 100644 --- a/src/app/domains/pos/layouts/layout.component.html +++ b/src/app/domains/pos/layouts/layout.component.html @@ -1,5 +1,7 @@ + + @@ -20,7 +22,7 @@
- +
diff --git a/src/app/domains/pos/layouts/layout.component.ts b/src/app/domains/pos/layouts/layout.component.ts index b638b76..1d6c4e5 100644 --- a/src/app/domains/pos/layouts/layout.component.ts +++ b/src/app/domains/pos/layouts/layout.component.ts @@ -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; @ViewChild('topbarCenter', { static: true }) topbarCenter!: TemplateRef; @ViewChild('topbarEnd', { static: true }) topbarEnd!: TemplateRef; @@ -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(); + } } diff --git a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts index 240544b..87a1650 100644 --- a/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts +++ b/src/app/domains/pos/modules/landing/components/payment/form-dialog.component.ts @@ -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 {} diff --git a/src/app/domains/pos/modules/landing/services/payment-bridge.abstract.ts b/src/app/domains/pos/modules/landing/services/payment-bridge.abstract.ts index d0917e9..4d02702 100644 --- a/src/app/domains/pos/modules/landing/services/payment-bridge.abstract.ts +++ b/src/app/domains/pos/modules/landing/services/payment-bridge.abstract.ts @@ -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; } diff --git a/src/app/domains/pos/modules/landing/services/payment-bridge.service.ts b/src/app/domains/pos/modules/landing/services/payment-bridge.service.ts index 7e4507f..a39bae3 100644 --- a/src/app/domains/pos/modules/landing/services/payment-bridge.service.ts +++ b/src/app/domains/pos/modules/landing/services/payment-bridge.service.ts @@ -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); } } diff --git a/src/app/domains/superAdmin/constants/menuItems.const.ts b/src/app/domains/superAdmin/constants/menuItems.const.ts index 2d7a4cf..4346ff1 100644 --- a/src/app/domains/superAdmin/constants/menuItems.const.ts +++ b/src/app/domains/superAdmin/constants/menuItems.const.ts @@ -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'], }, diff --git a/src/app/domains/superAdmin/modules/consumers/components/accounts/list.component.ts b/src/app/domains/superAdmin/modules/consumers/components/accounts/list.component.ts index a54766d..2b545cc 100644 --- a/src/app/domains/superAdmin/modules/consumers/components/accounts/list.component.ts +++ b/src/app/domains/superAdmin/modules/consumers/components/accounts/list.component.ts @@ -18,7 +18,7 @@ export class ConsumerAccountListComponent extends AbstractList { @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', diff --git a/src/app/domains/superAdmin/modules/consumers/components/poses/list.component.ts b/src/app/domains/superAdmin/modules/consumers/components/poses/list.component.ts index f767b10..db80714 100644 --- a/src/app/domains/superAdmin/modules/consumers/components/poses/list.component.ts +++ b/src/app/domains/superAdmin/modules/consumers/components/poses/list.component.ts @@ -22,7 +22,7 @@ export class ConsumerPosesComponent extends AbstractList { @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' } }, diff --git a/src/app/domains/superAdmin/modules/consumers/views/list.component.html b/src/app/domains/superAdmin/modules/consumers/views/list.component.html index 180216d..8a59a38 100644 --- a/src/app/domains/superAdmin/modules/consumers/views/list.component.html +++ b/src/app/domains/superAdmin/modules/consumers/views/list.component.html @@ -5,7 +5,6 @@ [items]="items()" [loading]="loading()" [showDetails]="true" - [showEdit]="true" (onAdd)="openAddForm()" (onDetails)="toSinglePage($event)" (onEdit)="onEditClick($event)" diff --git a/src/app/domains/superAdmin/modules/consumers/views/list.component.ts b/src/app/domains/superAdmin/modules/consumers/views/list.component.ts index 94ddf80..d3f66f5 100644 --- a/src/app/domains/superAdmin/modules/consumers/views/list.component.ts +++ b/src/app/domains/superAdmin/modules/consumers/views/list.component.ts @@ -19,7 +19,7 @@ export class ConsumersComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'name', header: 'نام' }, { field: 'business_counts', header: 'تعداد فعالیت‌های اقتصادی' }, { diff --git a/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.html b/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.html index fb5f139..e324232 100644 --- a/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.html +++ b/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.html @@ -7,7 +7,8 @@ (onHide)="close()" >
- + + diff --git a/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.ts b/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.ts index bdaeffb..f06c4ac 100644 --- a/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.ts +++ b/src/app/domains/superAdmin/modules/deviceBrands/components/form.component.ts @@ -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(this.initialValues?.name || '', { - nonNullable: true, - validators: [Validators.required], - }), + name: fieldControl.name(this.initialValues?.name), }); preparedTitle = computed(() => `${this.editMode ? 'ویرایش' : 'افزودن'} برند دستگاه`); diff --git a/src/app/domains/superAdmin/modules/deviceBrands/views/list.component.ts b/src/app/domains/superAdmin/modules/deviceBrands/views/list.component.ts index 90882e7..9d2ad7d 100644 --- a/src/app/domains/superAdmin/modules/deviceBrands/views/list.component.ts +++ b/src/app/domains/superAdmin/modules/deviceBrands/views/list.component.ts @@ -16,7 +16,7 @@ export class DeviceBrandsComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'name', header: 'عنوان' }, { field: 'created_at', diff --git a/src/app/domains/superAdmin/modules/devices/components/form.component.html b/src/app/domains/superAdmin/modules/devices/components/form.component.html index 6f31857..3ddc879 100644 --- a/src/app/domains/superAdmin/modules/devices/components/form.component.html +++ b/src/app/domains/superAdmin/modules/devices/components/form.component.html @@ -7,7 +7,7 @@ (onHide)="close()" >
- + diff --git a/src/app/domains/superAdmin/modules/devices/components/form.component.ts b/src/app/domains/superAdmin/modules/devices/components/form.component.ts index a6a9e1c..299eff8 100644 --- a/src/app/domains/superAdmin/modules/devices/components/form.component.ts +++ b/src/app/domains/superAdmin/modules/devices/components/form.component.ts @@ -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 { diff --git a/src/app/domains/superAdmin/modules/devices/views/list.component.ts b/src/app/domains/superAdmin/modules/devices/views/list.component.ts index da2bb11..85bc35e 100644 --- a/src/app/domains/superAdmin/modules/devices/views/list.component.ts +++ b/src/app/domains/superAdmin/modules/devices/views/list.component.ts @@ -16,7 +16,7 @@ export class DevicesComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'name', header: 'عنوان' }, { field: 'brand', diff --git a/src/app/domains/superAdmin/modules/guilds/components/categories/list.component.ts b/src/app/domains/superAdmin/modules/guilds/components/categories/list.component.ts index 3c989a6..f3c01a8 100644 --- a/src/app/domains/superAdmin/modules/guilds/components/categories/list.component.ts +++ b/src/app/domains/superAdmin/modules/guilds/components/categories/list.component.ts @@ -18,7 +18,7 @@ export class GuildGoodCategoriesListComponent extends AbstractList +
+ + + + + + + + diff --git a/src/app/domains/superAdmin/modules/guilds/components/stockKeepingUnits/form.component.ts b/src/app/domains/superAdmin/modules/guilds/components/stockKeepingUnits/form.component.ts new file mode 100644 index 0000000..a7bc1f5 --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/components/stockKeepingUnits/form.component.ts @@ -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); + } +} diff --git a/src/app/domains/superAdmin/modules/guilds/constants/apiRoutes/stockKeepingUnits.ts b/src/app/domains/superAdmin/modules/guilds/constants/apiRoutes/stockKeepingUnits.ts new file mode 100644 index 0000000..d2de5a4 --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/constants/apiRoutes/stockKeepingUnits.ts @@ -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}`, +}; diff --git a/src/app/domains/superAdmin/modules/guilds/constants/routes/index.ts b/src/app/domains/superAdmin/modules/guilds/constants/routes/index.ts index 781b814..a7c4977 100644 --- a/src/app/domains/superAdmin/modules/guilds/constants/routes/index.ts +++ b/src/app/domains/superAdmin/modules/guilds/constants/routes/index.ts @@ -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), diff --git a/src/app/domains/superAdmin/modules/guilds/constants/routes/stockKeepingUnits.ts b/src/app/domains/superAdmin/modules/guilds/constants/routes/stockKeepingUnits.ts new file mode 100644 index 0000000..1fd7b1b --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/constants/routes/stockKeepingUnits.ts @@ -0,0 +1,20 @@ +import { NamedRoutes } from '@/core'; +import { Routes } from '@angular/router'; + +export type TStockKeepingUnitsRouteNames = 'stockKeepingUnits'; + +export const stockKeepingUnitsNamedRoutes: NamedRoutes = { + 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); diff --git a/src/app/domains/superAdmin/modules/guilds/models/index.ts b/src/app/domains/superAdmin/modules/guilds/models/index.ts index 5447473..bc5c96c 100644 --- a/src/app/domains/superAdmin/modules/guilds/models/index.ts +++ b/src/app/domains/superAdmin/modules/guilds/models/index.ts @@ -1,2 +1,3 @@ export * from './goods_io'; export * from './io'; +export * from './stockKeepingUnits_io'; diff --git a/src/app/domains/superAdmin/modules/guilds/models/io.d.ts b/src/app/domains/superAdmin/modules/guilds/models/io.d.ts index 638964f..9cedce4 100644 --- a/src/app/domains/superAdmin/modules/guilds/models/io.d.ts +++ b/src/app/domains/superAdmin/modules/guilds/models/io.d.ts @@ -9,6 +9,7 @@ export interface IGuildResponse extends IGuildRawResponse {} export interface IGuildRequest { name: string; code: string; + invoice_template_type: string; } export interface IGoodCategoriesRawResponse { diff --git a/src/app/domains/superAdmin/modules/guilds/models/stockKeepingUnits_io.d.ts b/src/app/domains/superAdmin/modules/guilds/models/stockKeepingUnits_io.d.ts new file mode 100644 index 0000000..a800316 --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/models/stockKeepingUnits_io.d.ts @@ -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; +} diff --git a/src/app/domains/superAdmin/modules/guilds/services/stockKeepingUnits.service.ts b/src/app/domains/superAdmin/modules/guilds/services/stockKeepingUnits.service.ts new file mode 100644 index 0000000..65e2822 --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/services/stockKeepingUnits.service.ts @@ -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> { + return this.http.get>( + this.apiRoutes.list(guildId), + ); + } + + create( + guildId: string, + payload: IStockKeepingUnitRequest, + ): Observable { + return this.http.post(this.apiRoutes.list(guildId), payload); + } + + update( + guildId: string, + id: string, + payload: IStockKeepingUnitRequest, + ): Observable { + return this.http.patch(this.apiRoutes.single(guildId, id), payload); + } +} diff --git a/src/app/domains/superAdmin/modules/guilds/views/categories/list.component.ts b/src/app/domains/superAdmin/modules/guilds/views/categories/list.component.ts index 972db06..8239b4c 100644 --- a/src/app/domains/superAdmin/modules/guilds/views/categories/list.component.ts +++ b/src/app/domains/superAdmin/modules/guilds/views/categories/list.component.ts @@ -9,6 +9,7 @@ import { GuildGoodCategoriesListComponent } from '../../components/categories/li imports: [GuildGoodCategoriesListComponent], }) export class GuildGoodCategoriesComponent { - route = inject(ActivatedRoute); - guildId = signal(this.route.snapshot.paramMap.get('guildId')!); + private readonly route = inject(ActivatedRoute); + + readonly guildId = signal(this.route.snapshot.paramMap.get('guildId')!); } diff --git a/src/app/domains/superAdmin/modules/guilds/views/list.component.ts b/src/app/domains/superAdmin/modules/guilds/views/list.component.ts index e2ac61d..a0db32f 100644 --- a/src/app/domains/superAdmin/modules/guilds/views/list.component.ts +++ b/src/app/domains/superAdmin/modules/guilds/views/list.component.ts @@ -24,7 +24,7 @@ export class GuildsComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'name', header: 'عنوان' }, { field: 'code', header: 'کد' }, { field: 'businessActivitiesCount', header: 'تعداد فعالیت‌‌های اقتصادی‌ مرتبط' }, diff --git a/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.html b/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.html new file mode 100644 index 0000000..e809729 --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.html @@ -0,0 +1,22 @@ + + + diff --git a/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.ts b/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.ts new file mode 100644 index 0000000..a0e5efa --- /dev/null +++ b/src/app/domains/superAdmin/modules/guilds/views/stockKeepingUnits/list.component.ts @@ -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 { + private readonly route = inject(ActivatedRoute); + pageParams = computed(() => pageParamsUtils(this.route)); + readonly guildId = signal(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()); + } +} diff --git a/src/app/domains/superAdmin/modules/partners/components/accounts/list.component.ts b/src/app/domains/superAdmin/modules/partners/components/accounts/list.component.ts index a198396..97f1326 100644 --- a/src/app/domains/superAdmin/modules/partners/components/accounts/list.component.ts +++ b/src/app/domains/superAdmin/modules/partners/components/accounts/list.component.ts @@ -18,7 +18,7 @@ export class ConsumerAccountListComponent extends AbstractList { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, + // { field: 'id', header: 'شناسه', type: 'id' }, { field: 'name', header: 'نام' }, { field: 'created_at', diff --git a/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.html b/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.html index 152ea1f..6fccce0 100644 --- a/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.html +++ b/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.html @@ -1,8 +1,14 @@ - +
- + diff --git a/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.ts b/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.ts index b25eddf..0af3131 100644 --- a/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.ts +++ b/src/app/domains/superAdmin/modules/stockKeepingUnits/components/form.component.ts @@ -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'; diff --git a/src/app/domains/superAdmin/modules/stockKeepingUnits/constants/routes/index.ts b/src/app/domains/superAdmin/modules/stockKeepingUnits/constants/routes/index.ts index fcfdd93..6ce2c25 100644 --- a/src/app/domains/superAdmin/modules/stockKeepingUnits/constants/routes/index.ts +++ b/src/app/domains/superAdmin/modules/stockKeepingUnits/constants/routes/index.ts @@ -9,7 +9,7 @@ export const stockKeepingUnitsNamedRoutes: NamedRoutes import('../../views/list.component').then((m) => m.StockKeepingUnitsComponent), meta: { - title: 'واحدهای شمارش کالا', + title: 'واحدهای شناسه کالا', pagePath: () => '/super_admin/stock-keeping-units', }, }, diff --git a/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html b/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html index b15e17a..b7b9e3e 100644 --- a/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html +++ b/src/app/domains/superAdmin/modules/stockKeepingUnits/views/list.component.html @@ -2,7 +2,6 @@ [pageTitle]="'مدیریت شناسه کالاها'" [addNewCtaLabel]="'افزودن شناسه کالا'" [columns]="columns" - [showAdd]="true" emptyPlaceholderTitle="شناسه کالایی یافت نشد" emptyPlaceholderDescription="برای افزودن شناسه کالا، روی دکمهٔ بالا کلیک کنید." [items]="items()" @@ -12,13 +11,11 @@ (onAdd)="openAddForm()" (onRefresh)="refresh()" > - @if (selectedItemForEdit()) { - - } + diff --git a/src/app/domains/superAdmin/modules/users/components/accounts/list.component.ts b/src/app/domains/superAdmin/modules/users/components/accounts/list.component.ts index e91c24d..d86ac8a 100644 --- a/src/app/domains/superAdmin/modules/users/components/accounts/list.component.ts +++ b/src/app/domains/superAdmin/modules/users/components/accounts/list.component.ts @@ -18,7 +18,6 @@ export class UsersComponent extends AbstractList { @Input() userId!: string; @Input() fullHeight?: boolean; @Input() header: IColumn[] = [ - { field: 'id', header: 'شناسه', type: 'id' }, { field: 'username', header: 'نام کاربری' }, { field: 'type', header: 'نوع حساب' }, { diff --git a/src/app/domains/superAdmin/modules/users/views/list.component.html b/src/app/domains/superAdmin/modules/users/views/list.component.html index a31ee52..24961af 100644 --- a/src/app/domains/superAdmin/modules/users/views/list.component.html +++ b/src/app/domains/superAdmin/modules/users/views/list.component.html @@ -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()" /> { override setColumns(): void { this.columns = [ - { field: 'id', header: 'شناسه', type: 'id' }, { field: 'fullname', header: 'نام' }, { field: 'mobile_number', header: 'شماره موبایل' }, { field: 'national_code', header: 'کد ملی' }, diff --git a/src/app/layout/default/app.topbar.component.ts b/src/app/layout/default/app.topbar.component.ts index 68524cb..0c40f91 100644 --- a/src/app/layout/default/app.topbar.component.ts +++ b/src/app/layout/default/app.topbar.component.ts @@ -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; diff --git a/src/app/shared/components/fields/vat.component.ts b/src/app/shared/components/fields/vat.component.ts index 15f1003..ec865f4 100644 --- a/src/app/shared/components/fields/vat.component.ts +++ b/src/app/shared/components/fields/vat.component.ts @@ -4,7 +4,12 @@ import { InputComponent } from '../input/input.component'; @Component({ selector: 'field-vat', - template: ``, + template: ``, imports: [ReactiveFormsModule, InputComponent], }) export class VatComponent { diff --git a/src/app/shared/components/good/form.component.html b/src/app/shared/components/good/form.component.html new file mode 100644 index 0000000..057ee98 --- /dev/null +++ b/src/app/shared/components/good/form.component.html @@ -0,0 +1,19 @@ + + + @if (visible) { + + } + + + + + + + + + + diff --git a/src/app/shared/components/good/form.component.ts b/src/app/shared/components/good/form.component.ts new file mode 100644 index 0000000..4480615 --- /dev/null +++ b/src/app/shared/components/good/form.component.ts @@ -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; + @Input({ required: true }) updateFn!: (payload: FormData) => Observable; + + goodImage = signal>(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); + } +} diff --git a/src/app/shared/components/good/index.ts b/src/app/shared/components/good/index.ts new file mode 100644 index 0000000..820937a --- /dev/null +++ b/src/app/shared/components/good/index.ts @@ -0,0 +1,2 @@ +export * from './form.component'; +export * from './types'; diff --git a/src/app/shared/components/good/types.ts b/src/app/shared/components/good/types.ts new file mode 100644 index 0000000..cde6f2f --- /dev/null +++ b/src/app/shared/components/good/types.ts @@ -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; +} diff --git a/src/app/shared/components/input/input.component.ts b/src/app/shared/components/input/input.component.ts index a3b3a71..0c6db4a 100644 --- a/src/app/shared/components/input/input.component.ts +++ b/src/app/shared/components/input/input.component.ts @@ -67,6 +67,7 @@ export class InputComponent { @Input() maxLength?: number; @Input() min?: number; @Input() max?: number; + @Input() fixed?: number; @Output() valueChange = new EventEmitter(); @Output() blur = new EventEmitter(); @@ -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 = 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); } } } diff --git a/src/app/shared/components/key-value.component/key-value.component.html b/src/app/shared/components/key-value.component/key-value.component.html index d1377de..017ec2b 100644 --- a/src/app/shared/components/key-value.component/key-value.component.html +++ b/src/app/shared/components/key-value.component/key-value.component.html @@ -10,8 +10,8 @@ }" > - @if (valueType() === "badge") { - + @if (valueType() === "tag") { + } @else { {{ valueToShow() }} } diff --git a/src/app/shared/components/key-value.component/key-value.component.ts b/src/app/shared/components/key-value.component/key-value.component.ts index 66d3fec..62a6cf3 100644 --- a/src/app/shared/components/key-value.component/key-value.component.ts +++ b/src/app/shared/components/key-value.component/key-value.component.ts @@ -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', ); } diff --git a/src/app/shared/components/pageDataList/page-data-list-grid-view.component.html b/src/app/shared/components/pageDataList/page-data-list-grid-view.component.html index ec1e326..c011f33 100644 --- a/src/app/shared/components/pageDataList/page-data-list-grid-view.component.html +++ b/src/app/shared/components/pageDataList/page-data-list-grid-view.component.html @@ -17,7 +17,7 @@
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) { @if (col.type !== "index") { - + @if (col.type === "thumbnail") {
}
+ } @else if (col.variant === "tag") { + } @else if (getTemplate(col)) { { - 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 | ((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 { DrawerModule, UikitCopyComponent, KeyValueComponent, + Tag, ], }) export class AppPageDataListGridView { diff --git a/src/app/shared/components/pageDataList/page-data-list-table-view.component.html b/src/app/shared/components/pageDataList/page-data-list-table-view.component.html index 256b7f3..621391c 100644 --- a/src/app/shared/components/pageDataList/page-data-list-table-view.component.html +++ b/src/app/shared/components/pageDataList/page-data-list-table-view.component.html @@ -43,11 +43,11 @@ - - @if (showIndex) { + @if (showIndex) { + {{ i * (currentPage || 1) + 1 }} - } - + + } @for (col of columns; track col.field) { @if (col.type === "thumbnail") { @@ -59,6 +59,8 @@ }
+ } @else if (col.variant === "tag") { + } @else if (getTemplate(col)) { } @@ -211,6 +214,11 @@ @for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) { + @if (showIndex) { + + + + } @for (col of columns; track col) { diff --git a/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts b/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts index 22d3478..6eedbf1 100644 --- a/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts +++ b/src/app/shared/components/pageDataList/page-data-list-table-view.component.ts @@ -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 { - 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 | ((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 { DrawerModule, UikitCopyComponent, KeyValueComponent, + Tag, ], }) export class AppPageDataListTableView { diff --git a/src/app/shared/components/pageDataList/page-data-list.component.html b/src/app/shared/components/pageDataList/page-data-list.component.html index 85d3a3d..642f541 100644 --- a/src/app/shared/components/pageDataList/page-data-list.component.html +++ b/src/app/shared/components/pageDataList/page-data-list.component.html @@ -13,6 +13,7 @@ [showEdit]="showEdit" [showDelete]="showDelete" [showDetails]="showDetails" + [showIndex]="showIndex" (onEdit)="edit($event)" (onDelete)="remove($event)" (onDetails)="details($event)" diff --git a/src/app/shared/components/pageDataList/page-data-list.component.ts b/src/app/shared/components/pageDataList/page-data-list.component.ts index 9d61ca3..e736434 100644 --- a/src/app/shared/components/pageDataList/page-data-list.component.ts +++ b/src/app/shared/components/pageDataList/page-data-list.component.ts @@ -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 { field: T extends object ? keyof T | string : string; header: string; @@ -39,6 +45,11 @@ export interface IColumn { 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 { @Input() expandableItemKey?: string = ''; @Input() expandColumns?: Maybe = null; @Input() dataKey?: string = 'items'; + @Input() showIndex?: boolean = true; @ContentChild('filter', { static: true }) filter!: TemplateRef | null; @ContentChild('caption', { static: true }) caption!: TemplateRef | null; diff --git a/src/app/shared/constants/fields/index.ts b/src/app/shared/constants/fields/index.ts index 456556b..613f0eb 100644 --- a/src/app/shared/constants/fields/index.ts +++ b/src/app/shared/constants/fields/index.ts @@ -169,4 +169,9 @@ export const fieldControl = { value, isRequired ? required() : [], ], + + measure_unit: (value = '', isRequired = true): ControlConfig => [ + value, + isRequired ? required() : [], + ], }; diff --git a/src/app/shared/localEnum/constants/goldKarat.ts b/src/app/shared/localEnum/constants/goldKarat.ts index 3da0544..c279148 100644 --- a/src/app/shared/localEnum/constants/goldKarat.ts +++ b/src/app/shared/localEnum/constants/goldKarat.ts @@ -8,9 +8,9 @@ export const goldKarat = { export type TGoldKarat = (typeof goldKarat)[keyof typeof goldKarat]; const translates: Record = { - KARAT_18: '18', - KARAT_21: '21', - KARAT_24: '24', + KARAT_18: 'عیار ۱۸', + KARAT_21: 'عیار ۲۱', + KARAT_24: 'عیار ۲۴', }; export const goldKaratSelect: ISelectItem[] = Object.keys(goldKarat).map((key) => ({ diff --git a/src/assets/layout/_menu.scss b/src/assets/layout/_menu.scss index 7bf56d1..612b76d 100644 --- a/src/assets/layout/_menu.scss +++ b/src/assets/layout/_menu.scss @@ -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;