Compare commits

...

3 Commits

59 changed files with 647 additions and 385 deletions
+1
View File
@@ -202,6 +202,7 @@ Never output:
- "I wonder..." - "I wonder..."
- "Maybe..." - "Maybe..."
- implementation deliberation - implementation deliberation
- "It sounds..."
Never explain alternative approaches unless requested. Never explain alternative approaches unless requested.
+1 -32
View File
@@ -44,9 +44,6 @@ importers:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@zoomit/dayjs-jalali-plugin':
specifier: ^0.1.11
version: 0.1.11
angularx-qrcode: angularx-qrcode:
specifier: 20.0.0 specifier: 20.0.0
version: 20.0.0(@angular/core@20.3.19(@angular/compiler@20.3.19)(rxjs@7.8.2)) version: 20.0.0(@angular/core@20.3.19(@angular/compiler@20.3.19)(rxjs@7.8.2))
@@ -56,12 +53,6 @@ importers:
dayjs: dayjs:
specifier: ^1.11.20 specifier: ^1.11.20
version: 1.11.20 version: 1.11.20
flatpickr:
specifier: ^4.6.13
version: 4.6.13
flatpickr-wrap:
specifier: ^1.0.0
version: 1.0.0
jalaliday: jalaliday:
specifier: ^3.1.1 specifier: ^3.1.1
version: 3.1.1(dayjs@1.11.20) version: 3.1.1(dayjs@1.11.20)
@@ -2817,9 +2808,6 @@ packages:
'@yarnpkg/lockfile@1.1.0': '@yarnpkg/lockfile@1.1.0':
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
'@zoomit/dayjs-jalali-plugin@0.1.11':
resolution: {integrity: sha512-xqbrBPRoBhbaAWeLbqq9QERZwj+h2KuJMehpOQrlT4y980oA+rdiF/YzXlzVaIvc845UYDrsTp8IDwttWyV/xg==}
abbrev@4.0.0: abbrev@4.0.0:
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
engines: {node: ^20.17.0 || >=22.9.0} engines: {node: ^20.17.0 || >=22.9.0}
@@ -3956,12 +3944,6 @@ packages:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true hasBin: true
flatpickr-wrap@1.0.0:
resolution: {integrity: sha512-ctthNBZyDJe1P2fpLF7N7ivwDQGkWAAr7R5BLZ2BSlz9O+w4NEc68M7FNvK/UPUPNj3FQ2pGV8UNZV/9WBU9/Q==}
flatpickr@4.6.13:
resolution: {integrity: sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==}
flatted@3.4.2: flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
@@ -4229,7 +4211,7 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
iconv-lite@0.7.2: iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==, tarball: https://hub.megan.ir/repository/npm/iconv-lite/-/iconv-lite-0.7.2.tgz}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
icss-utils@5.1.0: icss-utils@5.1.0:
@@ -5085,9 +5067,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
native-date-adapter@1.0.3:
resolution: {integrity: sha512-WXA6496dHy79BS35CWbeBha1A1QV5LjezaTA5bFWV3LXrwiRT9YXE5LJc+SSEgbN7QHz++xJnjhMHZAXMKIqAg==}
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -9450,8 +9429,6 @@ snapshots:
'@yarnpkg/lockfile@1.1.0': {} '@yarnpkg/lockfile@1.1.0': {}
'@zoomit/dayjs-jalali-plugin@0.1.11': {}
abbrev@4.0.0: {} abbrev@4.0.0: {}
abort-controller@3.0.0: abort-controller@3.0.0:
@@ -10884,12 +10861,6 @@ snapshots:
flat@5.0.2: {} flat@5.0.2: {}
flatpickr-wrap@1.0.0:
dependencies:
native-date-adapter: 1.0.3
flatpickr@4.6.13: {}
flatted@3.4.2: {} flatted@3.4.2: {}
follow-redirects@1.16.0(debug@4.4.3): follow-redirects@1.16.0(debug@4.4.3):
@@ -12106,8 +12077,6 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
native-date-adapter@1.0.3: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
needle@3.5.0: needle@3.5.0:
@@ -68,6 +68,13 @@ export class FormErrorsService {
const info = errors['invalidUsername']; const info = errors['invalidUsername'];
out.push({ key: 'invalidUsername', message: info.message ?? `${label} معتبر نیست.` }); out.push({ key: 'invalidUsername', message: info.message ?? `${label} معتبر نیست.` });
} }
if (errors['equalLength']) {
const info = errors['equalLength'];
out.push({
key: 'equalLength',
message: `تعداد کاراکترهای ${label} باید برابر با ${info.length} باشد.`,
});
}
if (errors['invalidFiscalId']) { if (errors['invalidFiscalId']) {
const info = errors['invalidFiscalId']; const info = errors['invalidFiscalId'];
+8
View File
@@ -10,10 +10,18 @@ interface IToast extends Pick<ToastMessageOptions, 'sticky' | 'life'> {
providedIn: 'root', providedIn: 'root',
}) })
export class ToastService { export class ToastService {
private readonly dedupeWindowMs = 2000;
private readonly recentMessages = new Map<string, number>();
constructor(private messageService: MessageService) {} constructor(private messageService: MessageService) {}
add(message: ToastMessageOptions) { add(message: ToastMessageOptions) {
if (!message.detail) return; if (!message.detail) return;
const dedupeKey = `${message.severity ?? ''}|${message.summary ?? ''}|${message.detail}`;
const now = Date.now();
const lastShownAt = this.recentMessages.get(dedupeKey);
if (lastShownAt && now - lastShownAt < this.dedupeWindowMs) return;
this.recentMessages.set(dedupeKey, now);
this.messageService.add(message); this.messageService.add(message);
} }
@@ -0,0 +1,19 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function equalLengthValidator(length: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const v = control.value;
if (v === null || v === undefined || v === '') {
return null;
}
const normalized = String(v);
return normalized.length !== length
? {
equalLength: {
length,
},
}
: null;
};
}
+1
View File
@@ -1,3 +1,4 @@
export * from './equalLength.validator';
export * from './fiscal-id.validator'; export * from './fiscal-id.validator';
export * from './greater.validator'; export * from './greater.validator';
export * from './iban.validator'; export * from './iban.validator';
@@ -4,11 +4,10 @@
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ width: '500px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<field-name [control]="form.controls.name" /> <field-name [control]="form.controls.name" />
<field-economic-code [control]="form.controls.economic_code" /> <field-individual-economic-code [control]="form.controls.economic_code" />
<field-fiscal-id [control]="form.controls.fiscal_id" /> <field-fiscal-id [control]="form.controls.fiscal_id" />
<field-partner-token [control]="form.controls.partner_token" /> <field-partner-token [control]="form.controls.partner_token" />
<field-invoice-number-sequence [control]="form.controls.invoice_number_sequence" [min]="1" /> <field-invoice-number-sequence [control]="form.controls.invoice_number_sequence" [min]="1" />
@@ -1,7 +1,7 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { import {
EconomicCodeComponent,
FiscalIdComponent, FiscalIdComponent,
IndividualEconomicCodeComponent,
NameComponent, NameComponent,
PartnerTokenComponent, PartnerTokenComponent,
} from '@/shared/components'; } from '@/shared/components';
@@ -22,7 +22,7 @@ import { BusinessActivitiesService } from '../services/main.service';
SharedDialogComponent, SharedDialogComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
NameComponent, NameComponent,
EconomicCodeComponent, IndividualEconomicCodeComponent,
PartnerTokenComponent, PartnerTokenComponent,
InvoiceNumberSequenceComponent, InvoiceNumberSequenceComponent,
FiscalIdComponent, FiscalIdComponent,
@@ -37,11 +37,11 @@ export class ConsumerBusinessActivityFormComponent extends AbstractFormDialog<
form = this.fb.group({ form = this.fb.group({
name: fieldControl.name(this.initialValues?.name || ''), name: fieldControl.name(this.initialValues?.name || ''),
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''), economic_code: fieldControl.individual_economic_code(this.initialValues?.economic_code || ''),
fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''), fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''),
partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''), partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''),
invoice_number_sequence: fieldControl.invoice_number_sequence( invoice_number_sequence: fieldControl.invoice_number_sequence(
this.initialValues?.invoice_number_sequence || 1, this.initialValues?.invoice_number_sequence || 1
), ),
}); });
@@ -1,6 +1,6 @@
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<field-name [control]="form.controls.name" /> <field-name [control]="form.controls.name" />
<field-economic-code [control]="form.controls.economic_code" /> <field-individual-economic-code [control]="form.controls.economic_code" />
<field-fiscal-id [control]="form.controls.fiscal_id" /> <field-fiscal-id [control]="form.controls.fiscal_id" />
<field-invoice-number-sequence [control]="form.controls.invoice_number_sequence" /> <field-invoice-number-sequence [control]="form.controls.invoice_number_sequence" />
<field-partner-token [control]="form.controls.partner_token" /> <field-partner-token [control]="form.controls.partner_token" />
@@ -1,8 +1,8 @@
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { import {
EconomicCodeComponent,
FiscalIdComponent, FiscalIdComponent,
GuildIdComponent, GuildIdComponent,
IndividualEconomicCodeComponent,
LicenseStartsAtComponent, LicenseStartsAtComponent,
NameComponent, NameComponent,
PartnerTokenComponent, PartnerTokenComponent,
@@ -22,7 +22,7 @@ import { PartnerConsumerBusinessActivitiesService } from '../../services/busines
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
NameComponent, NameComponent,
EconomicCodeComponent, IndividualEconomicCodeComponent,
FiscalIdComponent, FiscalIdComponent,
PartnerTokenComponent, PartnerTokenComponent,
GuildIdComponent, GuildIdComponent,
@@ -44,10 +44,10 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractForm<
initForm = () => { initForm = () => {
const form = this.fb.group({ const form = this.fb.group({
name: fieldControl.name(this.initialValues?.name || ''), name: fieldControl.name(this.initialValues?.name || ''),
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''), economic_code: fieldControl.individual_economic_code(this.initialValues?.economic_code || ''),
fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''), fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''),
invoice_number_sequence: fieldControl.invoice_number_sequence( invoice_number_sequence: fieldControl.invoice_number_sequence(
parseInt(this.initialValues?.invoice_number_sequence || '1'), parseInt(this.initialValues?.invoice_number_sequence || '1')
), ),
partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''), partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''),
guild_id: fieldControl.guild_id(this.initialValues?.guild?.id || ''), guild_id: fieldControl.guild_id(this.initialValues?.guild?.id || ''),
@@ -63,7 +63,7 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractForm<
this.fb.control<string>('', { this.fb.control<string>('', {
nonNullable: true, nonNullable: true,
validators: fieldControl.license_starts_at('')[1], validators: fieldControl.license_starts_at('')[1],
}), })
); );
} }
@@ -5,12 +5,11 @@
<ng-template #topbarCenter> <ng-template #topbarCenter>
@if (posInfo()) { @if (posInfo()) {
<a [routerLink]="homeRouteLink" class="flex flex-col items-center justify-center gap-2"> <a [routerLink]="homeRouteLink" class="flex flex-col items-center justify-center gap-2">
<div class="w-8 h-8"> <div class="h-8 w-8">
<img <img
[src]="posInfo()?.partner?.logo_url ?? placeholderLogo" [src]="posInfo()?.partner?.logo_url ?? placeholderLogo"
alt="Logo" alt="Logo"
class="w-full h-full object-cover rounded-md bg-surface-card" class="bg-surface-card h-full w-full rounded-md object-cover" />
/>
</div> </div>
<span class="text-base font-semibold">{{ posInfo()?.partner?.name }}</span> <span class="text-base font-semibold">{{ posInfo()?.partner?.name }}</span>
</a> </a>
@@ -19,7 +18,7 @@
<ng-template #topbarEnd> <ng-template #topbarEnd>
<div class=""> <div class="">
<p-menu #menu [model]="profileMenuItems" [popup]="true" /> <p-menu #menu [model]="profileMenuItems" [popup]="true" appendTo="body" />
<p-button (click)="menu.toggle($event)" icon="pi pi-user" outlined size="large" /> <p-button (click)="menu.toggle($event)" icon="pi pi-user" outlined size="large" />
</div> </div>
</ng-template> </ng-template>
@@ -57,11 +56,11 @@
<pos-choose-cards class="h-full" (onChoose)="onChoosePos()" /> <pos-choose-cards class="h-full" (onChoose)="onChoosePos()" />
} }
@default { @default {
<div class="h-full w-full flex items-center justify-center"> <div class="flex h-full w-full items-center justify-center">
<p-card class="border border-surface-border"> <p-card class="border-surface-border border">
<div class="flex flex-col gap-2 items-center justify-between shrink-0"> <div class="flex shrink-0 flex-col items-center justify-between gap-2">
<i class="pi pi-exclamation-triangle text-6xl! mb-3 text-error" aria-hidden="true"></i> <i class="pi pi-exclamation-triangle text-error mb-3 text-6xl!" aria-hidden="true"></i>
<span class="text-xl font-semibold mb-2 text-error block"> عدم دسترسی </span> <span class="text-error mb-2 block text-xl font-semibold"> عدم دسترسی </span>
<p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p> <p class="inline-block text-base">متاسفانه امکان دسترسی برای کاربر شما فراهم نیست</p>
<button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button> <button pButton outlined severity="danger" class="mt-5 w-32" (click)="logout()">خروج</button>
</div> </div>
@@ -1,7 +1,6 @@
<app-input <app-price-input
[control]="goldPrice" [control]="goldPrice"
label="قیمت پیش‌فرض هر گرم طلا" label="قیمت پیش‌فرض هر گرم طلا"
name="goldBasePrice" name="goldBasePrice"
type="price"
[min]="0" [min]="0"
hint="در این بخش می‌توانید قیمت پیش‌فرض هر گرم طلا را وارد کنید." /> hint="در این بخش می‌توانید قیمت پیش‌فرض هر گرم طلا را وارد کنید." />
@@ -1,4 +1,4 @@
import { InputComponent } from '@/shared/components'; import { PriceInputComponent } from '@/shared/components/input/price-input.component';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { debounce, timer } from 'rxjs'; import { debounce, timer } from 'rxjs';
@@ -7,7 +7,7 @@ import { PosConfigGoldPriceService } from './services/main.service';
@Component({ @Component({
selector: 'pos-config-gold-price-form', selector: 'pos-config-gold-price-form',
templateUrl: 'form.component.html', templateUrl: 'form.component.html',
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, PriceInputComponent],
}) })
export class PosConfigGoldPriceFormComponent { export class PosConfigGoldPriceFormComponent {
private readonly service = inject(PosConfigGoldPriceService); private readonly service = inject(PosConfigGoldPriceService);
@@ -48,8 +48,8 @@
</div> --> </div> -->
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<app-input label="از مبلغ" [control]="form.controls.total_amount_from" name="total_amount_from" type="price" /> <app-price-input label="از مبلغ" [control]="form.controls.total_amount_from" name="total_amount_from" />
<app-input label="تا مبلغ" [control]="form.controls.total_amount_to" name="total_amount_to" type="price" /> <app-price-input label="تا مبلغ" [control]="form.controls.total_amount_to" name="total_amount_to" />
</div> </div>
</form> </form>
<div class="bg-surface-card mt-auto shrink-0 pb-4"> <div class="bg-surface-card mt-auto shrink-0 pb-4">
@@ -2,6 +2,7 @@ import { Maybe } from '@/core';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component'; import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { TspProviderResponseStatus } from '@/shared/catalog'; import { TspProviderResponseStatus } from '@/shared/catalog';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { PriceInputComponent } from '@/shared/components/input/price-input.component';
import { DatepickerComponent } from '@/uikit'; import { DatepickerComponent } from '@/uikit';
import { nowGregorian } from '@/utils'; import { nowGregorian } from '@/utils';
import { import {
@@ -32,6 +33,7 @@ import { IPosSaleInvoicesFilterDto } from '../models';
EnumSelectComponent, EnumSelectComponent,
InputComponent, InputComponent,
DatepickerComponent, DatepickerComponent,
PriceInputComponent,
], ],
}) })
export class PosSaleInvoicesFilterDrawerComponent implements OnChanges { export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
@@ -54,4 +54,4 @@
[visible]="filterVisible()" [visible]="filterVisible()"
[value]="filters()" [value]="filters()"
(visibleChange)="filterVisible.set($event)" (visibleChange)="filterVisible.set($event)"
(apply)="onFilterApply($event)" /> (apply)="changeFilter($event)" />
@@ -112,6 +112,12 @@ export class PosSaleInvoiceListComponent {
this.filterVisible.update((v) => !v); this.filterVisible.update((v) => !v);
} }
changeFilter(query: IPosSaleInvoicesFilterDto) {
const { page, ...rest } = query;
this.resetPage();
this.onFilterApply(rest);
}
onFilterApply(query: IPosSaleInvoicesFilterDto) { onFilterApply(query: IPosSaleInvoicesFilterDto) {
this.filters.set(query); this.filters.set(query);
this.syncFiltersToQueryParams(); this.syncFiltersToQueryParams();
@@ -119,6 +125,7 @@ export class PosSaleInvoiceListComponent {
} }
removeFilter(key: keyof IPosSaleInvoicesFilterDto) { removeFilter(key: keyof IPosSaleInvoicesFilterDto) {
this.resetPage();
const next = { ...this.filters() }; const next = { ...this.filters() };
delete next[key]; delete next[key];
this.filters.set(next); this.filters.set(next);
@@ -126,6 +133,10 @@ export class PosSaleInvoiceListComponent {
this.getData(); this.getData();
} }
resetPage() {
this.filters.update((val) => ({ ...val, page: 1 }));
}
toSinglePage(item: IPosSaleInvoicesResponse) { toSinglePage(item: IPosSaleInvoicesResponse) {
this.router.navigateByUrl(posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(item.id)); this.router.navigateByUrl(posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(item.id));
} }
@@ -160,7 +171,6 @@ export class PosSaleInvoiceListComponent {
} }
private syncFiltersToQueryParams() { private syncFiltersToQueryParams() {
this.filters.update((val) => ({ ...val, page: 1 }));
const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>( const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>(
(acc, [key, value]) => { (acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') { if (value !== undefined && value !== null && value !== '') {
@@ -1,9 +1,10 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<p-message severity="info"> وارد کردن شماره اقتصادی و یا کد ملی به همراه کد پستی الزامی است. </p-message>
<field-first-name [control]="form.controls.first_name" /> <field-first-name [control]="form.controls.first_name" />
<field-last-name [control]="form.controls.last_name" /> <field-last-name [control]="form.controls.last_name" />
<field-mobile-number [control]="form.controls.mobile_number" /> <field-individual-economic-code [control]="form.controls.economic_code" />
<field-national-id [control]="form.controls.national_id" /> <field-national-id [control]="form.controls.national_id" />
<field-economic-code [control]="form.controls.economic_code" />
<field-postal-code [control]="form.controls.postal_code" /> <field-postal-code [control]="form.controls.postal_code" />
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" /> <app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" />
@@ -1,9 +1,8 @@
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { import {
EconomicCodeComponent,
FirstNameComponent, FirstNameComponent,
IndividualEconomicCodeComponent,
LastNameComponent, LastNameComponent,
MobileNumberComponent,
NationalIdComponent, NationalIdComponent,
PostalCodeComponent, PostalCodeComponent,
} from '@/shared/components'; } from '@/shared/components';
@@ -11,7 +10,8 @@ import { FormFooterActionsComponent } from '@/shared/components/formFooterAction
import { fieldControl } from '@/shared/constants'; import { fieldControl } from '@/shared/constants';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { AbstractControl, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
import { Message } from 'primeng/message';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { IIndividualCustomer } from '../../../models/customer'; import { IIndividualCustomer } from '../../../models/customer';
import { PosLandingStore } from '../../../store/main.store'; import { PosLandingStore } from '../../../store/main.store';
@@ -24,10 +24,10 @@ import { PosLandingStore } from '../../../store/main.store';
FormFooterActionsComponent, FormFooterActionsComponent,
FirstNameComponent, FirstNameComponent,
LastNameComponent, LastNameComponent,
NationalIdComponent, IndividualEconomicCodeComponent,
EconomicCodeComponent,
MobileNumberComponent,
PostalCodeComponent, PostalCodeComponent,
Message,
NationalIdComponent,
], ],
}) })
export class CustomerIndividualFormComponent extends AbstractForm< export class CustomerIndividualFormComponent extends AbstractForm<
@@ -38,14 +38,56 @@ export class CustomerIndividualFormComponent extends AbstractForm<
costumerInfo = computed(() => this.store.customer().info?.customer_individual); costumerInfo = computed(() => this.store.customer().info?.customer_individual);
override showSuccessMessage = false; override showSuccessMessage = false;
form = this.fb.group({ private readonly identityOrEconomicCodeValidator = (
first_name: fieldControl.first_name(this.costumerInfo()?.first_name || ''), control: AbstractControl
last_name: fieldControl.last_name(this.costumerInfo()?.last_name || ''), ): ValidationErrors | null => {
national_id: fieldControl.national_id(this.costumerInfo()?.national_id || ''), const economicCode = control.get('economic_code')?.value;
postal_code: fieldControl.postal_code(this.costumerInfo()?.postal_code || ''), const nationalId = control.get('national_id')?.value;
economic_code: fieldControl.economic_code(this.costumerInfo()?.economic_code || ''), const postalCode = control.get('postal_code')?.value;
mobile_number: fieldControl.mobile_number(this.costumerInfo()?.mobile_number || ''),
}); const hasEconomicCode = !!String(economicCode ?? '').trim();
const hasNationalId = !!String(nationalId ?? '').trim();
const hasPostalCode = !!String(postalCode ?? '').trim();
const isValid = hasEconomicCode || (hasNationalId && hasPostalCode);
if (!isValid) {
return { identityOrEconomicCodeRequired: true };
}
return null;
};
form = this.fb.group(
{
first_name: fieldControl.first_name(this.costumerInfo()?.first_name || '', false),
last_name: fieldControl.last_name(this.costumerInfo()?.last_name || '', false),
economic_code: fieldControl.individual_economic_code(
this.costumerInfo()?.economic_code || '',
false
),
national_id: fieldControl.national_id(this.costumerInfo()?.national_id || '', false),
postal_code: fieldControl.postal_code(this.costumerInfo()?.postal_code || '', false),
},
{ validators: this.identityOrEconomicCodeValidator }
);
override submit() {
this.form.markAllAsTouched();
this.form.updateValueAndValidity({ onlySelf: false, emitEvent: false });
if (this.form.hasError('identityOrEconomicCodeRequired')) {
this.toastService.warn({
text: 'وارد کردن شماره اقتصادی و یا کد ملی به همراه کد پستی الزامی است.',
});
}
if (this.form.invalid) {
return;
}
super.submit();
}
override submitForm() { override submitForm() {
return new Observable<IIndividualCustomer>((observer) => { return new Observable<IIndividualCustomer>((observer) => {
@@ -54,7 +96,10 @@ export class CustomerIndividualFormComponent extends AbstractForm<
customer_id: '', customer_id: '',
type: CustomerType.INDIVIDUAL, type: CustomerType.INDIVIDUAL,
info: { info: {
customer_individual: info, customer_individual: {
...info,
economic_code: info.economic_code ? info.economic_code.toString() : undefined,
},
}, },
}); });
return observer.next(info); return observer.next(info);
@@ -1,8 +1,11 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.company_name" name="company_name" label="نام مجموعه" /> <p-message severity="info" icon="pi pi-info-circle">
<app-input [control]="form.controls.registration_number" name="registration_number" label="شماره ثبت" /> وارد کردن شماره اقتصادی و یا شناسه‌ ملی به همراه کد پستی الزامی است.
<app-input [control]="form.controls.economic_code" name="economic_code" label="کد اقتصادی" maxlength="11" /> </p-message>
<app-input [control]="form.controls.postal_code" name="postal_code" label="کد پستی" type="postalCode" /> <field-name [control]="form.controls.name" name="name" label="نام مجموعه" />
<field-legal-economic-code [control]="form.controls.economic_code" />
<field-registration-number [control]="form.controls.registration_number" />
<field-postal-code [control]="form.controls.postal_code" />
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" /> <app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" />
</form> </form>
@@ -1,10 +1,16 @@
import { postalCodeValidator } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import {
LegalEconomicCodeComponent,
NameComponent,
PostalCodeComponent,
RegistrationNumberComponent,
} from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { fieldControl } from '@/shared/constants';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { AbstractControl, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
import { Message } from 'primeng/message';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ILegalCustomer } from '../../../models/customer'; import { ILegalCustomer } from '../../../models/customer';
import { PosLandingStore } from '../../../store/main.store'; import { PosLandingStore } from '../../../store/main.store';
@@ -12,31 +18,76 @@ import { PosLandingStore } from '../../../store/main.store';
@Component({ @Component({
selector: 'pos-customer-legal-form', selector: 'pos-customer-legal-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent], imports: [
ReactiveFormsModule,
FormFooterActionsComponent,
LegalEconomicCodeComponent,
NameComponent,
RegistrationNumberComponent,
PostalCodeComponent,
Message,
],
}) })
export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILegalCustomer> { export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILegalCustomer> {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
override showSuccessMessage = false; override showSuccessMessage = false;
form = this.fb.group({ private readonly registrationOrEconomicCodeValidator = (
company_name: [ control: AbstractControl
this.store.customer().info?.customer_legal?.company_name || '', ): ValidationErrors | null => {
[Validators.required], const economicCode = control.get('economic_code')?.value;
], const registrationNumber = control.get('registration_number')?.value;
registration_number: [ const postalCode = control.get('postal_code')?.value;
this.store.customer().info?.customer_legal?.registration_number || '',
[Validators.required], const hasEconomicCode = !!String(economicCode ?? '').trim();
], const hasRegistrationNumber = !!String(registrationNumber ?? '').trim();
economic_code: [ const hasPostalCode = !!String(postalCode ?? '').trim();
this.store.customer().info?.customer_legal?.economic_code || '',
[Validators.required], const isValid = hasEconomicCode || (hasRegistrationNumber && hasPostalCode);
],
postal_code: [ if (!isValid) {
this.store.customer().info?.customer_legal?.postal_code || '', return { registrationOrEconomicCodeRequired: true };
[postalCodeValidator()], }
],
}); return null;
};
form = this.fb.group(
{
name: fieldControl.name(this.store.customer().info?.customer_legal?.name || ''),
economic_code: fieldControl.legal_economic_code(
this.store.customer().info?.customer_legal?.economic_code || '',
false
),
registration_number: fieldControl.registration_number(
this.store.customer().info?.customer_legal?.registration_number || '',
false
),
postal_code: fieldControl.postal_code(
this.store.customer().info?.customer_legal?.postal_code || '',
false
),
},
{ validators: this.registrationOrEconomicCodeValidator }
);
override submit() {
this.form.markAllAsTouched();
this.form.updateValueAndValidity({ onlySelf: false, emitEvent: false });
if (this.form.hasError('registrationOrEconomicCodeRequired')) {
this.toastService.warn({
text: 'وارد کردن شماره اقتصادی و یا شناسه‌ ملی به همراه کد پستی الزامی است.',
});
}
if (this.form.invalid) {
return;
}
super.submit();
}
override submitForm() { override submitForm() {
return new Observable<ILegalCustomer>((observer) => { return new Observable<ILegalCustomer>((observer) => {
@@ -45,7 +96,10 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
customer_id: '', customer_id: '',
type: CustomerType.LEGAL, type: CustomerType.LEGAL,
info: { info: {
customer_legal: info, customer_legal: {
...info,
economic_code: info.economic_code ? info.economic_code.toString() : undefined,
},
}, },
}); });
return observer.next(info); return observer.next(info);
@@ -1,5 +1,5 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.name" name="name" label="نام مشتری" /> <app-input [control]="form.controls.name" name="name" label="نام مشتری" />
<app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" /> <app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" />
<app-form-footer-actions submitLabel="ثبت بدون اطلاعات خریدار و ادامه" (onCancel)="close()" /> <app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" />
</form> </form>
@@ -0,0 +1,3 @@
<shared-light-bottomsheet [(visible)]="visible" [closable]="true" header="جزییات صورت‌حساب‌">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet>
@@ -0,0 +1,21 @@
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedLightBottomsheetComponent } from '@/shared/components';
import { Component, EventEmitter, Output } from '@angular/core';
import { PosOrderSectionComponent } from './order-section.component';
@Component({
selector: 'pos-order-section-dialog',
templateUrl: 'order-section-dialog.component.html',
imports: [SharedLightBottomsheetComponent, PosOrderSectionComponent],
})
export class PosOrderSectionDialogComponent extends AbstractDialog {
@Output() onAddMoreGoods = new EventEmitter();
@Output() onSubmit = new EventEmitter();
closeInvoiceBottomSheet = () => {
this.onAddMoreGoods.emit();
};
submitOrder = () => {
this.onSubmit.emit();
};
}
@@ -64,7 +64,7 @@ export class PosOrderSectionComponent {
customerNameToShow = `${info?.customer_individual ? info?.customer_individual.first_name + ' ' + info?.customer_individual.last_name : '-----'} (نوع اول)`; customerNameToShow = `${info?.customer_individual ? info?.customer_individual.first_name + ' ' + info?.customer_individual.last_name : '-----'} (نوع اول)`;
break; break;
case 'LEGAL': case 'LEGAL':
customerNameToShow = `${info?.customer_legal?.company_name || '-----'} (نوع اول)`; customerNameToShow = `${info?.customer_legal?.name || '-----'} (نوع اول)`;
break; break;
} }
@@ -85,6 +85,8 @@ export class PosOrderSectionComponent {
} }
submitAndPay() { submitAndPay() {
console.log('submitAndPay');
this.onSubmit.emit(); this.onSubmit.emit();
} }
@@ -36,7 +36,7 @@
(onChange)="changePayByTerminalSteps($event.value)" /> (onChange)="changePayByTerminalSteps($event.value)" />
</uikit-field> </uikit-field>
@for (terminalControl of terminalControls; track $index) { @for (terminalControl of terminalControls; track $index) {
<app-input <app-price-input
[control]="terminalControl" [control]="terminalControl"
[name]="'terminal_' + $index" [name]="'terminal_' + $index"
[label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)" [label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
@@ -53,30 +53,29 @@
باقی‌مانده مبلغ باقی‌مانده مبلغ
</button> </button>
</ng-template> </ng-template>
</app-input> </app-price-input>
} }
</div> </div>
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input <app-price-input
[control]="form.controls.cash" [control]="form.controls.cash"
name="cash" name="cash"
label="نقدی/ کارت‌خوان دیگر/ کارت به کارت" label="نقدی/ کارت‌خوان دیگر/ کارت به کارت"
type="price"
[max]="cashMax()"> [max]="cashMax()">
<ng-template #suffixTemp> <ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')"> <button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
باقی‌مانده مبلغ باقی‌مانده مبلغ
</button> </button>
</ng-template> </ng-template>
</app-input> </app-price-input>
@if (isGoldGuild()) { @if (isGoldGuild()) {
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()"> <app-price-input [control]="form.controls.set_off" name="setOff" label="تهاتر" [max]="setOffMax()">
<ng-template #suffixTemp> <ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')"> <button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
باقی‌مانده مبلغ باقی‌مانده مبلغ
</button> </button>
</ng-template> </ng-template>
</app-input> </app-price-input>
} }
<app-form-footer-actions <app-form-footer-actions
@@ -3,9 +3,10 @@ import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { PosInfoStore } from '@/domains/pos/store'; import { PosInfoStore } from '@/domains/pos/store';
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent, KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component'; import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { PriceInputComponent } from '@/shared/components/input/price-input.component';
import { UikitFieldComponent, UikitLabelComponent } from '@/uikit'; import { UikitFieldComponent, UikitLabelComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -31,7 +32,6 @@ import { PosLandingStore } from '../../store/main.store';
imports: [ imports: [
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
InputComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
KeyValueComponent, KeyValueComponent,
ButtonDirective, ButtonDirective,
@@ -40,6 +40,7 @@ import { PosLandingStore } from '../../store/main.store';
UikitFieldComponent, UikitFieldComponent,
SelectButton, SelectButton,
UikitLabelComponent, UikitLabelComponent,
PriceInputComponent,
], ],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }], providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
}) })
@@ -7,7 +7,12 @@
} }
} @else { } @else {
@for (good of visibleGoods(); track good.id) { @for (good of visibleGoods(); track good.id) {
<div class="bg-surface-card border-surface-border flex flex-1 flex-col rounded-lg border p-1"> <div class="bg-surface-card border-surface-border relative flex flex-1 flex-col rounded-lg border p-1">
@if (!good.is_default_guild_good) {
<div class="absolute inset-e-2 top-2">
<p-tag severity="contrast" value="شخصی" />
</div>
}
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
loading="lazy" loading="lazy"
@@ -16,9 +21,6 @@
<div class="mt-2 flex grow flex-col justify-between text-center"> <div class="mt-2 flex grow flex-col justify-between text-center">
<span class="flex w-full grow items-center justify-center text-center text-lg font-bold"> <span class="flex w-full grow items-center justify-center text-center text-lg font-bold">
{{ good.name }} {{ good.name }}
@if (!good.is_default_guild_good) {
<small class="text-muted-color text-xs">*</small>
}
</span> </span>
<div class="my-2 mt-1 flex w-full shrink-0 items-center justify-between gap-1 px-2"> <div class="my-2 mt-1 flex w-full shrink-0 items-center justify-between gap-1 px-2">
<button pButton type="button" label="انتخاب" class="w-full grow" (click)="addProduct(good)"></button> <button pButton type="button" label="انتخاب" class="w-full grow" (click)="addProduct(good)"></button>
@@ -11,6 +11,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton'; import { Skeleton } from 'primeng/skeleton';
import { Tag } from 'primeng/tag';
import images from 'src/assets/images'; import images from 'src/assets/images';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
import { FavoriteCTAComponent } from './favorite-CTA.component'; import { FavoriteCTAComponent } from './favorite-CTA.component';
@@ -19,7 +20,7 @@ import { FavoriteCTAComponent } from './favorite-CTA.component';
selector: 'pos-good-grid-view', selector: 'pos-good-grid-view',
templateUrl: './grid-view.component.html', templateUrl: './grid-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonDirective, Skeleton, FavoriteCTAComponent], imports: [ButtonDirective, Skeleton, FavoriteCTAComponent, Tag],
}) })
export class PosGoodsGridViewComponent { export class PosGoodsGridViewComponent {
@Output() onAdd = new EventEmitter<IGoodResponse>(); @Output() onAdd = new EventEmitter<IGoodResponse>();
@@ -17,9 +17,9 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-base font-bold"> <span class="text-base font-bold">
{{ good.name }} {{ good.name }}
@if (!good.is_default_guild_good) { <!-- @if (!good.is_default_guild_good) {
<small class="text-muted-color text-xs">*</small> <p-tag severity="contrast" value="شخصی" />
} } -->
</span> </span>
<span class="text-muted-color text-sm"> <span class="text-muted-color text-sm">
{{ good.sku.code }} {{ good.sku.code }}
@@ -11,6 +11,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton'; import { Skeleton } from 'primeng/skeleton';
import { Tag } from 'primeng/tag';
import images from 'src/assets/images'; import images from 'src/assets/images';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
import { FavoriteCTAComponent } from './favorite-CTA.component'; import { FavoriteCTAComponent } from './favorite-CTA.component';
@@ -19,7 +20,7 @@ import { FavoriteCTAComponent } from './favorite-CTA.component';
selector: 'pos-goods-list-view', selector: 'pos-goods-list-view',
templateUrl: './list-view.component.html', templateUrl: './list-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonDirective, Skeleton, FavoriteCTAComponent], imports: [ButtonDirective, Skeleton, FavoriteCTAComponent, Tag],
}) })
export class PosGoodsListViewComponent { export class PosGoodsListViewComponent {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
@@ -1,17 +1,16 @@
export interface IIndividualCustomer { export interface IIndividualCustomer {
first_name: string; first_name?: string;
last_name: string; last_name?: string;
national_id: string; national_id?: string;
postal_code: string; postal_code?: string;
mobile_number: string; economic_code?: string;
economic_code: string;
is_favorite?: boolean; is_favorite?: boolean;
} }
export interface ILegalCustomer { export interface ILegalCustomer {
company_name: string; name?: string;
economic_code: string; economic_code?: string;
registration_number: string; registration_number?: string;
postal_code: string; postal_code?: string;
is_favorite?: boolean; is_favorite?: boolean;
} }
@@ -33,10 +33,12 @@
</div> </div>
</button> </button>
} }
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات صورت‌حساب‌"> @if (showInvoiceBottomSheet()) {
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" /> <pos-order-section-dialog
</shared-light-bottomsheet> [(visible)]="showInvoiceBottomSheet"
(onAddMoreGoods)="closeInvoiceBottomSheet()"
(onSubmit)="submitOrder()" />
}
@if (isVisiblePaymentForm()) { @if (isVisiblePaymentForm()) {
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" /> <pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
} }
@@ -2,11 +2,11 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store/pos.store'; import { PosInfoStore } from '@/domains/pos/store/pos.store';
import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-mobile'; import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-mobile';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { PosOrderSectionDialogComponent } from '../components/order/order-section-dialog.component';
import { PosOrderSectionComponent } from '../components/order/order-section.component'; import { PosOrderSectionComponent } from '../components/order/order-section.component';
import { PosOrderSubmittedDialogComponent } from '../components/order/order-submitted-dialog.component'; import { PosOrderSubmittedDialogComponent } from '../components/order/order-submitted-dialog.component';
import { PosPaymentFormDialogComponent } from '../components/payment/form-dialog.component'; import { PosPaymentFormDialogComponent } from '../components/payment/form-dialog.component';
@@ -25,10 +25,10 @@ import { PosLandingStore } from '../store/main.store';
PosOrderSectionComponent, PosOrderSectionComponent,
PageLoadingComponent, PageLoadingComponent,
PriceMaskDirective, PriceMaskDirective,
SharedLightBottomsheetComponent,
ButtonDirective, ButtonDirective,
PosOrderSubmittedDialogComponent, PosOrderSubmittedDialogComponent,
PosPaymentFormDialogComponent, PosPaymentFormDialogComponent,
PosOrderSectionDialogComponent,
], ],
}) })
export class PosShopComponent extends AbstractIsMobileComponent { export class PosShopComponent extends AbstractIsMobileComponent {
@@ -4,11 +4,10 @@
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ width: '500px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<field-name [control]="form.controls.name" /> <field-name [control]="form.controls.name" />
<field-economic-code [control]="form.controls.economic_code" /> <field-individual-economic-code [control]="form.controls.economic_code" />
<field-fiscal-id [control]="form.controls.fiscal_id" /> <field-fiscal-id [control]="form.controls.fiscal_id" />
<field-partner-token [control]="form.controls.partner_token" /> <field-partner-token [control]="form.controls.partner_token" />
<field-guild-id [control]="form.controls.guild_id" /> <field-guild-id [control]="form.controls.guild_id" />
@@ -1,9 +1,9 @@
// import { CatalogRolesComponent } from '@/shared/catalog/roles'; // import { CatalogRolesComponent } from '@/shared/catalog/roles';
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { import {
EconomicCodeComponent,
FiscalIdComponent, FiscalIdComponent,
GuildIdComponent, GuildIdComponent,
IndividualEconomicCodeComponent,
NameComponent, NameComponent,
PartnerTokenComponent, PartnerTokenComponent,
} from '@/shared/components'; } from '@/shared/components';
@@ -22,7 +22,7 @@ import { AdminConsumerBusinessActivitiesService } from '../../services/businessA
ReactiveFormsModule, ReactiveFormsModule,
SharedDialogComponent, SharedDialogComponent,
NameComponent, NameComponent,
EconomicCodeComponent, IndividualEconomicCodeComponent,
FiscalIdComponent, FiscalIdComponent,
PartnerTokenComponent, PartnerTokenComponent,
GuildIdComponent, GuildIdComponent,
@@ -41,7 +41,7 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractFormDialog<
initForm = () => { initForm = () => {
return this.fb.group({ return this.fb.group({
name: fieldControl.name(this.initialValues?.name || ''), name: fieldControl.name(this.initialValues?.name || ''),
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''), economic_code: fieldControl.individual_economic_code(this.initialValues?.economic_code || ''),
fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''), fiscal_id: fieldControl.fiscal_id(this.initialValues?.fiscal_id || ''),
partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''), partner_token: fieldControl.partner_token(this.initialValues?.partner_token || ''),
// @ts-ignore // @ts-ignore
@@ -3,10 +3,9 @@
[name]="name" [name]="name"
[control]="selectedType.value === 'amount' ? amountControl : percentageControl" [control]="selectedType.value === 'amount' ? amountControl : percentageControl"
[showLabel]="!!label" [showLabel]="!!label"
[showErrors]="showErrors" [showErrors]="showErrors">
>
<ng-template #labelView> <ng-template #labelView>
<div class="flex gap-1 items-center justify-between"> <div class="flex items-center justify-between gap-1">
<uikit-label [name]="name"> <uikit-label [name]="name">
{{ preparedLabel() }} {{ preparedLabel() }}
</uikit-label> </uikit-label>
@@ -17,7 +16,7 @@
</ng-template> </ng-template>
<p-inputgroup> <p-inputgroup>
@if (selectedType.value === "amount") { @if (selectedType.value === 'amount') {
<p-inputnumber <p-inputnumber
#amountInput #amountInput
[attr.id]="name" [attr.id]="name"
@@ -25,11 +24,11 @@
[attr.name]="name" [attr.name]="name"
[attr.placeholder]="placeholder" [attr.placeholder]="placeholder"
[attr.autocomplete]="autocomplete" [attr.autocomplete]="autocomplete"
[attr.inputmode]="'numeric'"
[inputStyle]="{ direction: 'ltr' }" [inputStyle]="{ direction: 'ltr' }"
[inputStyleClass]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`" [inputStyleClass]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
[required]="required" [required]="required"
[invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)" [invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)" />
/>
} @else { } @else {
<input <input
#percentageInput #percentageInput
@@ -39,7 +38,6 @@
[attr.name]="name" [attr.name]="name"
[attr.placeholder]="placeholder" [attr.placeholder]="placeholder"
[attr.inputmode]="'numeric'" [attr.inputmode]="'numeric'"
[attr.type]="'number'"
dir="ltr" dir="ltr"
[attr.autocomplete]="autocomplete" [attr.autocomplete]="autocomplete"
[classList]=" [classList]="
@@ -49,8 +47,7 @@
].join(' ') ].join(' ')
" "
[required]="required" [required]="required"
[invalid]="percentageControl.invalid && (percentageControl.touched || percentageControl.dirty)" [invalid]="percentageControl.invalid && (percentageControl.touched || percentageControl.dirty)" />
/>
} }
<p-inputgroup-addon class="py-0!"> <p-inputgroup-addon class="py-0!">
@@ -60,8 +57,7 @@
tabindex="-1" tabindex="-1"
optionValue="value" optionValue="value"
size="small" size="small"
class="border-none! shadow-none! w-full!" class="w-full! border-none! shadow-none!" />
/>
</p-inputgroup-addon> </p-inputgroup-addon>
</p-inputgroup> </p-inputgroup>
@if (hint) { @if (hint) {
@@ -97,10 +97,44 @@ export class AmountPercentageInputComponent {
private lastValidAmount = 0; private lastValidAmount = 0;
private lastValidPercentage = 0; private lastValidPercentage = 0;
private parseValue(value: Maybe<any>) { private toEnglishDigits(value: Maybe<string | number>): string {
const normalized = `${value ?? ''}`.replace(/,/g, ''); const normalizedValue = value === null || value === undefined ? '' : String(value);
const parsed = parseFloat(normalized);
return Number.isFinite(parsed) ? parsed : 0; return normalizedValue
.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 normalizeLeadingZeros(value: string, allowDecimal: boolean): string {
if (!value) return value;
if (!allowDecimal) return value.replace(/^0+(?=\d)/, '');
if (value.includes('.')) {
const [integerPart = '0', decimalPart = ''] = value.split('.');
const normalizedInteger = integerPart.replace(/^0+(?=\d)/, '') || '0';
return `${normalizedInteger}.${decimalPart}`;
}
return value.replace(/^0+(?=\d)/, '');
}
private makeCleanValue(value: Maybe<string>) {
const allowDecimal = this.selectedType.value === 'percentage';
const cleanedValue = this.sanitizeNumericValue(value || '', allowDecimal);
const normalizedValue = this.normalizeLeadingZeros(cleanedValue, allowDecimal);
return !Number.isNaN(normalizedValue) ? normalizedValue : '0';
} }
private getRangeLabel(value: number, isPercentageType: boolean) { private getRangeLabel(value: number, isPercentageType: boolean) {
@@ -115,7 +149,7 @@ export class AmountPercentageInputComponent {
value: number, value: number,
isPercentageType: boolean, isPercentageType: boolean,
min = 0, min = 0,
max = Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER
) { ) {
if (value < min) { if (value < min) {
this.toastService.warn({ this.toastService.warn({
@@ -135,7 +169,7 @@ export class AmountPercentageInputComponent {
inputEl.value = value ? value.toLocaleString('en-US') : '0'; inputEl.value = value ? value.toLocaleString('en-US') : '0';
} }
private resetPercentageInputView(value: number) { private resetPercentageInputView(value: string) {
const inputEl = this.percentageInput?.nativeElement; const inputEl = this.percentageInput?.nativeElement;
if (!inputEl) return; if (!inputEl) return;
inputEl.value = `${value}`; inputEl.value = `${value}`;
@@ -156,26 +190,28 @@ export class AmountPercentageInputComponent {
private syncFromPercentage(rawValue: Maybe<any>) { private syncFromPercentage(rawValue: Maybe<any>) {
const { min, max } = this.getPercentageRange(); const { min, max } = this.getPercentageRange();
const percentageValue = this.parseValue(rawValue); const percentageValue = this.makeCleanValue(rawValue);
if (!this.isInRange(percentageValue, min, max)) {
this.showOutOfRangeToast(percentageValue, true, min, max); if (!this.isInRange(Number(percentageValue), min, max)) {
this.showOutOfRangeToast(Number(percentageValue), true, min, max);
this.percentageControl.setValue(this.lastValidPercentage); this.percentageControl.setValue(this.lastValidPercentage);
this.resetPercentageInputView(this.lastValidPercentage); this.resetPercentageInputView(this.lastValidPercentage.toString());
return; return;
} }
this.lastValidPercentage = percentageValue; this.lastValidPercentage = Number(percentageValue);
const amountValue = (this.baseAmount * percentageValue) / 100; const amountValue = (this.baseAmount * Number(percentageValue)) / 100;
this.lastValidAmount = Number.isFinite(amountValue) ? amountValue : 0; this.lastValidAmount = Number.isFinite(amountValue) ? amountValue : 0;
this.percentageControl.setValue(this.lastValidPercentage, { emitEvent: false }); this.percentageControl.setValue(this.lastValidPercentage, { emitEvent: false });
this.amountControl.setValue(this.lastValidAmount, { emitEvent: false }); this.amountControl.setValue(this.lastValidAmount, { emitEvent: false });
this.preparedLabel.set(this.prepareLabel(true)); this.preparedLabel.set(this.prepareLabel(true));
this.resetPercentageInputView(percentageValue);
} }
private syncFromAmount(rawValue: Maybe<any>) { private syncFromAmount(rawValue: Maybe<any>) {
const { min, max } = this.getAmountRange(); const { min, max } = this.getAmountRange();
const amountValue = this.parseValue(rawValue); const amountValue = Number(this.makeCleanValue(rawValue));
if (!this.isInRange(amountValue, min, max)) { if (!this.isInRange(amountValue, min, max)) {
this.showOutOfRangeToast(amountValue, false, min, max); this.showOutOfRangeToast(amountValue, false, min, max);
this.amountControl.setValue(this.lastValidAmount); this.amountControl.setValue(this.lastValidAmount);
@@ -187,7 +223,7 @@ export class AmountPercentageInputComponent {
const percentageValue = this.baseAmount const percentageValue = this.baseAmount
? ((amountValue / this.baseAmount) * 100).toFixed(2) ? ((amountValue / this.baseAmount) * 100).toFixed(2)
: '0'; : '0';
this.lastValidPercentage = this.parseValue(percentageValue); this.lastValidPercentage = Number(this.makeCleanValue(percentageValue));
this.amountControl.setValue(this.lastValidAmount, { emitEvent: false }); this.amountControl.setValue(this.lastValidAmount, { emitEvent: false });
this.percentageControl.setValue(percentageValue, { emitEvent: false }); this.percentageControl.setValue(percentageValue, { emitEvent: false });
@@ -222,8 +258,8 @@ export class AmountPercentageInputComponent {
} }
}); });
this.lastValidAmount = this.parseValue(this.amountControl.value); this.lastValidAmount = Number(this.makeCleanValue(this.amountControl.value));
this.lastValidPercentage = this.parseValue(this.percentageControl.value); this.lastValidPercentage = Number(this.makeCleanValue(this.percentageControl.value));
if (isPercentageType) { if (isPercentageType) {
this.syncFromPercentage(this.percentageControl.value); this.syncFromPercentage(this.percentageControl.value);
} else { } else {
@@ -1,13 +0,0 @@
import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component';
@Component({
selector: 'field-cash',
template: `<app-input label="نقدی" [control]="control" [name]="name" type="price" />`,
imports: [ReactiveFormsModule, InputComponent],
})
export class CashComponent {
@Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'cash';
}
+2 -5
View File
@@ -1,17 +1,17 @@
export * from './address.component'; export * from './address.component';
export * from './branch_code.component'; export * from './branch_code.component';
export * from './cash.component';
export * from './code.component'; export * from './code.component';
export * from './company_name.component'; export * from './company_name.component';
export * from './description.component'; export * from './description.component';
export * from './device_id.component'; export * from './device_id.component';
export * from './economic_code.component';
export * from './email.component'; export * from './email.component';
export * from './first_name.component'; export * from './first_name.component';
export * from './fiscal_id.component'; export * from './fiscal_id.component';
export * from './guild_id.component'; export * from './guild_id.component';
export * from './individual_economic_code.component';
export * from './invoice_templates.component'; export * from './invoice_templates.component';
export * from './last_name.component'; export * from './last_name.component';
export * from './legal_economic_code.component';
export * from './legal_name.component'; export * from './legal_name.component';
export * from './license_starts_at.component'; export * from './license_starts_at.component';
export * from './mobile.component'; export * from './mobile.component';
@@ -28,10 +28,7 @@ export * from './quantity.component';
export * from './registration_code.component'; export * from './registration_code.component';
export * from './registration_number.component'; export * from './registration_number.component';
export * from './serial_number.component'; export * from './serial_number.component';
export * from './set_off.component';
export * from './sku.component'; export * from './sku.component';
export * from './sku_type.component'; export * from './sku_type.component';
export * from './terminal.component';
export * from './unit_price.component';
export * from './username.component'; export * from './username.component';
export * from './vat.component'; export * from './vat.component';
@@ -3,11 +3,17 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component'; import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-economic-code', selector: 'field-individual-economic-code',
template: `<app-input label="کد اقتصادی" [control]="control" [name]="name" type="number" />`, template: `<app-input
label="شماره اقتصادی"
[control]="control"
[name]="name"
type="number"
[length]="14"
/>`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
export class EconomicCodeComponent { export class IndividualEconomicCodeComponent {
@Input({ required: true }) control = new FormControl<string>(''); @Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'economic_code'; @Input() name = 'individual_economic_code';
} }
@@ -3,18 +3,17 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component'; import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-unit-price', selector: 'field-legal-economic-code',
template: `<app-input template: `<app-input
label="مبلغ واحد" label="شماره اقتصادی"
[control]="control" [control]="control"
[name]="name" [name]="name"
[hint]="hint" type="number"
type="price" [length]="11"
/>`, />`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
export class UnitPriceComponent { export class LegalEconomicCodeComponent {
@Input({ required: true }) control = new FormControl<string>(''); @Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'unit_price'; @Input() name = 'legal_economic_code';
@Input() hint = '';
} }
@@ -1,13 +0,0 @@
import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component';
@Component({
selector: 'field-set-off',
template: `<app-input label="تهاتر" [control]="control" [name]="name" type="price" />`,
imports: [ReactiveFormsModule, InputComponent],
})
export class SetOffComponent {
@Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'set_off';
}
@@ -1,13 +0,0 @@
import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component';
@Component({
selector: 'field-terminal',
template: `<app-input label="پرداخت با پوز" [control]="control" [name]="name" type="price" />`,
imports: [ReactiveFormsModule, InputComponent],
})
export class TerminalComponent {
@Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'terminal';
}
@@ -1,5 +1,5 @@
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button pButton [label]="cancelLabel" severity="secondary" (click)="cancel()"></button> <button pButton [label]="cancelLabel" severity="secondary" type="button" (click)="cancel()"></button>
<button <button
pButton pButton
[label]="submitLabel" [label]="submitLabel"
@@ -17,48 +17,28 @@
<p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" /> <p-toggleSwitch [attr.disabled]="disabled" [formControl]="control" />
} @else { } @else {
<p-inputgroup> <p-inputgroup>
<!-- [minlength]="minlength || null" --> <input
@if (type === 'price') { pInputText
<p-inputnumber [attr.id]="name"
[attr.id]="name" [formControl]="control"
[formControl]="control" [attr.name]="name"
[maxlength]="preparedMaxLength || null" [attr.placeholder]="preparedPlaceholder"
[attr.name]="name" [attr.inputmode]="inputMode"
[attr.placeholder]="preparedPlaceholder" [attr.maxlength]="preparedMaxLength || null"
[attr.type]="htmlType" [attr.disabled]="disabled"
[attr.autocomplete]="autocomplete" [attr.autocomplete]="autocomplete"
[attr.disabled]="disabled" [classList]="
[class]="`w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`" [
[required]="isRequired" inputClass,
[invalid]="control.invalid && (control.touched || control.dirty)" 'w-full',
(onInput)="onPriceInput($event)" `${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`,
(onBlur)="blur.emit()" /> suffixTemp || preparedSuffix() ? 'hasSuffix' : '',
<!-- (input)="onInput($event)" --> ].join(' ')
} @else { "
<input [required]="isRequired"
pInputText [invalid]="control.invalid && (control.touched || control.dirty)"
[attr.id]="name" (onBlur)="blur.emit()"
[formControl]="control" (input)="onInput($event)" />
[attr.name]="name"
[attr.placeholder]="preparedPlaceholder"
[attr.inputmode]="inputMode"
[attr.maxlength]="preparedMaxLength || null"
[attr.type]="htmlType"
[attr.disabled]="disabled"
[attr.autocomplete]="autocomplete"
[classList]="
[
inputClass,
'w-full',
`${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`,
suffixTemp || preparedSuffix() ? 'hasSuffix' : '',
].join(' ')
"
[required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)"
(onBlur)="blur.emit()"
(input)="onInput($event)" />
}
@if (suffixTemp) { @if (suffixTemp) {
<ng-container [ngTemplateOutlet]="suffixTemp" /> <ng-container [ngTemplateOutlet]="suffixTemp" />
} @else if (preparedSuffix()) { } @else if (preparedSuffix()) {
@@ -17,7 +17,7 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputGroupModule } from 'primeng/inputgroup'; import { InputGroupModule } from 'primeng/inputgroup';
import { InputGroupAddon } from 'primeng/inputgroupaddon'; import { InputGroupAddon } from 'primeng/inputgroupaddon';
import { InputMaskModule } from 'primeng/inputmask'; import { InputMaskModule } from 'primeng/inputmask';
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber'; import { InputNumberModule } from 'primeng/inputnumber';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { KeyFilterModule } from 'primeng/keyfilter'; import { KeyFilterModule } from 'primeng/keyfilter';
import { ToggleSwitch } from 'primeng/toggleswitch'; import { ToggleSwitch } from 'primeng/toggleswitch';
@@ -51,8 +51,7 @@ export class InputComponent {
| 'email' | 'email'
| 'checkbox' | 'checkbox'
| 'switch' | 'switch'
| 'number' | 'number' = 'simple';
| 'price' = 'simple';
@Input() control!: FormControl<Maybe<any>>; @Input() control!: FormControl<Maybe<any>>;
@Input() name!: string; @Input() name!: string;
@Input() label: string = ''; @Input() label: string = '';
@@ -80,17 +79,8 @@ export class InputComponent {
private readonly toastService = inject(ToastService); private readonly toastService = inject(ToastService);
// onInput(ev: Event) {
// const v = (ev.target as HTMLInputElement).value;
// const newLocal = v || v == '0';
// // this.valueChange.emit(this.type === 'price' && newLocal ? parseFloat(v) : v);
// }
preparedSuffix = computed(() => { preparedSuffix = computed(() => {
if (this.type === 'price') { return this.suffix;
return 'ریال';
} else return this.suffix;
}); });
get preparedPlaceholder(): string | null { get preparedPlaceholder(): string | null {
@@ -106,30 +96,34 @@ export class InputComponent {
return 'شماره تلفن'; return 'شماره تلفن';
case 'email': case 'email':
return 'آدرس ایمیل خود را وارد کنید'; return 'آدرس ایمیل خود را وارد کنید';
case 'price':
return 'مبلغ';
default: default:
return ''; return '';
} }
} }
get inputMode(): string | null { get inputMode(): string {
switch (this.type) { switch (this.type) {
case 'price':
return 'decimal';
case 'number': case 'number':
case 'mobile': return 'decimal';
case 'phone':
case 'postalCode': case 'postalCode':
case 'nationalId': case 'nationalId':
return 'numeric'; return 'numeric';
case 'mobile':
case 'phone':
return 'tel';
case 'email':
return 'email';
case 'checkbox':
return 'checkbox';
case 'switch':
return 'radio';
default: default:
return 'text'; return 'text';
} }
} }
get isNumeric(): boolean { get isNumeric(): boolean {
return this.inputMode === 'numeric'; return ['number', 'decimal'].includes(this.inputMode);
} }
get preparedMaxLength(): number | null { get preparedMaxLength(): number | null {
@@ -153,7 +147,6 @@ export class InputComponent {
switch (this.type) { switch (this.type) {
case 'mobile': case 'mobile':
case 'phone': case 'phone':
case 'price':
case 'number': case 'number':
inputClass.push('ltrInput'); inputClass.push('ltrInput');
break; break;
@@ -174,14 +167,12 @@ export class InputComponent {
switch (this.type) { switch (this.type) {
case 'email': case 'email':
return 'email'; return 'email';
case 'mobile': // case 'mobile':
case 'phone': // case 'phone':
case 'postalCode': // case 'postalCode':
case 'nationalId': // case 'nationalId':
return 'number';
case 'price':
case 'number': case 'number':
return 'text'; return 'number';
case 'checkbox': case 'checkbox':
return 'checkbox'; return 'checkbox';
case 'switch': case 'switch':
@@ -195,10 +186,6 @@ export class InputComponent {
return this.required || this.control.hasValidator(Validators.required); return this.required || this.control.hasValidator(Validators.required);
} }
onPriceInput($event: InputNumberInputEvent) {
this.onInput($event.originalEvent, true);
}
private toEnglishDigits(value: string): string { private toEnglishDigits(value: string): string {
return value return value
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776)) .replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
@@ -207,6 +194,7 @@ export class InputComponent {
private sanitizeNumericValue(value: string, allowDecimal: boolean): string { private sanitizeNumericValue(value: string, allowDecimal: boolean): string {
const normalized = this.toEnglishDigits(value).replace(/,/g, ''); const normalized = this.toEnglishDigits(value).replace(/,/g, '');
const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, ''); const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '');
if (!allowDecimal) return cleaned; if (!allowDecimal) return cleaned;
@@ -245,76 +233,84 @@ export class InputComponent {
return value.replace(/^0+(?=\d)/, ''); return value.replace(/^0+(?=\d)/, '');
} }
onInput($event: Event, isPriceFormat?: boolean) { private enforceMaxLength(value: string): string {
if (this.type !== 'price' && this.type !== 'number') { if (!this.preparedMaxLength || value.length <= this.preparedMaxLength) return value;
return; return value.slice(0, this.preparedMaxLength);
}
private parseNumericValue(value: string): number {
return Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
}
private isOutOfRange(numericValue: number): { min: boolean; max: boolean } {
return {
min: !!((this.min || this.min === 0) && numericValue < this.min),
max: !!((this.max || this.max === 0) && numericValue > this.max),
};
}
private notifyRangeError(range: { min: boolean; max: boolean }) {
if (range.max) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند بیشتر از ${this.max} باشد.`,
});
} }
if (range.min) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min} باشد.`,
});
}
}
private isIdentifierField(): boolean {
return ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
}
private writeControlAndViewValue(target: HTMLInputElement, value: string, numericValue: number) {
const isIdentifierField = this.isIdentifierField();
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(controlValue);
target.value = value;
}
onInput($event: Event) {
const target = $event.target as HTMLInputElement | null; const target = $event.target as HTMLInputElement | null;
if (!target) return; if (!target) return;
let value = target.value ?? ''; let value = target.value ?? '';
const allowDecimal = this.type === 'price' || this.type === 'number'; const allowDecimal = this.type === 'number';
const isNumericMode = this.isNumeric;
if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) { if (isNumericMode) {
value = this.sanitizeNumericValue(value, allowDecimal); value = this.sanitizeNumericValue(value, allowDecimal);
value = this.applyFixed(value); value = this.applyFixed(value);
value = this.normalizeLeadingZeros(value, allowDecimal); value = this.normalizeLeadingZeros(value, allowDecimal);
}
if (this.preparedMaxLength && value.length > this.preparedMaxLength) { value = this.enforceMaxLength(value);
value = value.slice(0, this.preparedMaxLength);
}
let numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value); let numericValue = this.parseNumericValue(value);
const minValidator = (this.min || this.min === 0) && numericValue < this.min!;
const maxValidator = (this.max || this.max === 0) && numericValue > this.max!; const range = this.isOutOfRange(numericValue);
if ( if (value !== '' && (range.min || range.max)) {
(this.inputMode === 'numeric' || this.inputMode === 'decimal') && this.notifyRangeError(range);
value !== '' && value = this.lastValidNumericValue;
(minValidator || maxValidator) numericValue = this.parseNumericValue(value);
) { this.writeControlAndViewValue(target, value, numericValue);
if (maxValidator) { return;
this.toastService.warn({ } else {
text: `مقدار ${this.label} نمی‌تواند بیشتر از ${this.max} باشد.`, this.lastValidNumericValue = value;
});
} }
if (minValidator) {
this.toastService.warn({ this.writeControlAndViewValue(target, value, numericValue);
text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min} باشد.`, if (this.valueChange.observed) {
}); this.valueChange.emit(this.isIdentifierField() ? value : numericValue);
} }
value = this.lastValidNumericValue;
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
// const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
const isIdentifierField = !this.numericValue;
const restoredControlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(restoredControlValue);
target.value = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
return;
} else if (this.inputMode === 'numeric' || this.inputMode === 'decimal') {
this.lastValidNumericValue = value;
}
const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(controlValue);
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
target.value = viewValue;
if (
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
this.valueChange.observed
) {
this.valueChange.emit(isIdentifierField ? value : numericValue);
} }
} }
ngOnInit() { ngOnInit() {
const allowDecimal = this.type === 'price' || this.type === 'number'; const allowDecimal = this.type === 'number';
const initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal); const initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal);
this.lastValidNumericValue = initial; this.lastValidNumericValue = initial;
} }
@@ -0,0 +1,28 @@
<uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors">
<p-inputgroup>
<p-inputnumber
[attr.id]="name"
[formControl]="control"
[attr.name]="name"
[attr.placeholder]="preparedPlaceholder"
[attr.type]="'text'"
[attr.autocomplete]="autocomplete"
[attr.disabled]="disabled"
[class]="`ltrInput rtlPlaceholder hasSuffix w-full ${sizeClass}`"
inputStyleClass="hasSuffix"
[required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)"
(onInput)="onInput($event)"
(onBlur)="blur.emit()" />
@if (suffixTemp) {
<ng-container [ngTemplateOutlet]="suffixTemp" />
} @else {
<p-inputgroup-addon [ngClass]="{ 'border-red-400!': (control.touched || control.dirty) && control.invalid }">
ریال
</p-inputgroup-addon>
}
</p-inputgroup>
@if (hint) {
<small [attr.id]="name + '-help'" class="text-muted-color">{{ hint }}</small>
}
</uikit-field>
@@ -0,0 +1,105 @@
import { Maybe } from '@/core';
import { ToastService } from '@/core/services/toast.service';
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
import { CommonModule } from '@angular/common';
import {
Component,
ContentChild,
EventEmitter,
inject,
Input,
Output,
TemplateRef,
} from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputGroupAddon } from 'primeng/inputgroupaddon';
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
@Component({
selector: 'app-price-input',
standalone: true,
imports: [
ReactiveFormsModule,
InputGroupModule,
InputGroupAddon,
InputNumberModule,
UikitFieldComponent,
CommonModule,
],
templateUrl: './price-input.component.html',
})
export class PriceInputComponent {
@Input() control!: FormControl<Maybe<any>>;
@Input() name!: string;
@Input() label = '';
@Input() placeholder?: string;
@Input() disabled = false;
@Input() size?: 'small' | 'large';
@Input() autocomplete: string = 'off';
@Input() showErrors = true;
@Input() hint?: string;
@Input() min?: number = 0;
@Input() max?: number;
@Input() required = false;
@Output() inputEvent = new EventEmitter<InputNumberInputEvent>();
@Output() blur = new EventEmitter<void>();
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
private readonly toastService = inject(ToastService);
get preparedPlaceholder(): string {
return this.placeholder || 'مبلغ';
}
get isRequired(): boolean {
return this.required || this.control.hasValidator(Validators.required);
}
get sizeClass(): string {
return this.size === 'large' ? 'p-inputtext-lg' : this.size === 'small' ? 'p-inputtext-sm' : '';
}
onInput(event: InputNumberInputEvent) {
const eventValue = Number(event.value ?? 0);
const min = this.min ?? undefined;
const max = this.max ?? undefined;
const minValidator = min !== undefined && eventValue < min;
const maxValidator = max !== undefined && eventValue > max;
if (maxValidator) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند بیشتر از ${this.max!.toLocaleString('en-US')} باشد.`,
});
}
if (minValidator) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min!.toLocaleString('en-US')} باشد.`,
});
}
if (minValidator || maxValidator) {
const normalizedValue = parseFloat(
eventValue.toString().slice(0, eventValue.toString().length - 1) || '0'
);
this.control.setValue(normalizedValue);
// @ts-ignore
event.originalEvent.target.value = normalizedValue
? normalizedValue.toLocaleString('en-US')
: normalizedValue;
} else {
// @ts-ignore
event.originalEvent.target.value = eventValue
? eventValue.toLocaleString('en-US')
: eventValue;
}
this.inputEvent.emit({
...event,
value: eventValue,
});
}
}
@@ -6,6 +6,7 @@ import { PosInfoStore } from '@/domains/pos/store';
import { import {
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
CatalogTaxProviderStatusTagComponent, CatalogTaxProviderStatusTagComponent,
TspProviderResponseStatus,
} from '@/shared/catalog'; } from '@/shared/catalog';
import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import { import {
@@ -87,6 +88,7 @@ export class SharedSaleInvoiceSingleViewComponent {
@Output() onRefresh = new EventEmitter<void>(); @Output() onRefresh = new EventEmitter<void>();
@Output() onSendToTsp = new EventEmitter<Observable<any>>(); @Output() onSendToTsp = new EventEmitter<Observable<any>>();
@Output() onResendToTsp = new EventEmitter<Observable<any>>();
@Output() onInquiry = new EventEmitter<Observable<any>>(); @Output() onInquiry = new EventEmitter<Observable<any>>();
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>; @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
@@ -102,6 +104,7 @@ export class SharedSaleInvoiceSingleViewComponent {
this.showBackFromSale = this.showBackFromSale.bind(this); this.showBackFromSale = this.showBackFromSale.bind(this);
this.printInvoice = this.printInvoice.bind(this); this.printInvoice = this.printInvoice.bind(this);
this.send = this.send.bind(this); this.send = this.send.bind(this);
this.resend = this.resend.bind(this);
this.inquiry = this.inquiry.bind(this); this.inquiry = this.inquiry.bind(this);
} }
@@ -116,6 +119,9 @@ export class SharedSaleInvoiceSingleViewComponent {
send() { send() {
this.onSendToTsp.emit(); this.onSendToTsp.emit();
} }
resend() {
this.onResendToTsp.emit();
}
async showRevokeConfirmation() { async showRevokeConfirmation() {
await this.confirmationService.ask({ await this.confirmationService.ask({
header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`,
@@ -146,40 +152,46 @@ export class SharedSaleInvoiceSingleViewComponent {
if (changes['invoice'] && this.invoice) { if (changes['invoice'] && this.invoice) {
const invoiceStatus = this.invoice.status.value; const invoiceStatus = this.invoice.status.value;
const actions: MenuItem[] = [ const actions: MenuItem[] = [
{
label: 'ارسال مجدد صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.SEND_FAILURE,
command: this.resend,
},
{ {
label: 'ارسال صورت‌حساب', label: 'ارسال صورت‌حساب',
icon: 'pi pi-send', icon: 'pi pi-send',
neededStatus: 'NOT_SEND', neededStatus: TspProviderResponseStatus.NOT_SEND,
command: this.send, command: this.send,
}, },
{ {
label: 'استعلام وضعیت', label: 'استعلام وضعیت',
icon: 'pi pi-info', icon: 'pi pi-info',
neededStatus: 'FISCAL_QUEUED', neededStatus: TspProviderResponseStatus.FISCAL_QUEUED,
command: this.inquiry, command: this.inquiry,
}, },
{ {
label: 'دلایل خطا', label: 'دلایل خطا',
icon: 'pi pi-question', icon: 'pi pi-question',
neededStatus: 'FAILURE', neededStatus: TspProviderResponseStatus.FAILURE,
command: this.showErrors, command: this.showErrors,
}, },
{ {
label: 'ابطال', label: 'ابطال',
icon: 'pi pi-eraser', icon: 'pi pi-eraser',
neededStatus: 'SUCCESS', neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showRevokeConfirmation, command: this.showRevokeConfirmation,
}, },
{ {
label: 'اصلاح', label: 'اصلاح',
icon: 'pi pi-file-edit', icon: 'pi pi-file-edit',
neededStatus: 'SUCCESS', neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showCorrection, command: this.showCorrection,
}, },
{ {
label: 'برگشت از فروش', label: 'برگشت از فروش',
icon: 'pi pi-cart-minus', icon: 'pi pi-cart-minus',
neededStatus: 'SUCCESS', neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showBackFromSale, command: this.showBackFromSale,
}, },
{ {
@@ -181,8 +181,6 @@ export class PageDataListComponent<I = any> {
}; };
onPage = ($event: any) => { onPage = ($event: any) => {
console.log('$event', $event);
this.onChangePage.emit($event); this.onChangePage.emit($event);
}; };
@@ -1,6 +1,6 @@
<div class="form-group"> <div class="form-group">
<form [formGroup]="form"> <form [formGroup]="form">
<app-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" type="price" /> <app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" />
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" /> <app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" /> <app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" />
@@ -17,6 +17,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton'; import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton';
import { PriceInputComponent } from '../input/price-input.component';
import goldAmountCalculationUtil from './gold-amount-calculation.util'; import goldAmountCalculationUtil from './gold-amount-calculation.util';
@Component({ @Component({
@@ -31,6 +32,7 @@ import goldAmountCalculationUtil from './gold-amount-calculation.util';
AmountPercentageInputComponent, AmountPercentageInputComponent,
ButtonDirective, ButtonDirective,
CalculatedAmountCardComponent, CalculatedAmountCardComponent,
PriceInputComponent,
], ],
}) })
export class SharedGoldPayloadFormComponent extends AbstractForm< export class SharedGoldPayloadFormComponent extends AbstractForm<
@@ -1,5 +1,5 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" type="price" /> <app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" />
<app-input <app-input
[control]="form.controls.quantity" [control]="form.controls.quantity"
name="quantity" name="quantity"
@@ -12,6 +12,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { IPosOrderItem } from '../../../domains/pos/modules/shop/models'; import { IPosOrderItem } from '../../../domains/pos/modules/shop/models';
import { PriceInputComponent } from '../input/price-input.component';
@Component({ @Component({
selector: 'shared-standard-payload-form', selector: 'shared-standard-payload-form',
@@ -22,6 +23,7 @@ import { IPosOrderItem } from '../../../domains/pos/modules/shop/models';
AmountPercentageInputComponent, AmountPercentageInputComponent,
ButtonDirective, ButtonDirective,
CalculatedAmountCardComponent, CalculatedAmountCardComponent,
PriceInputComponent,
], ],
}) })
export class SharedStandardPayloadFormComponent extends AbstractForm< export class SharedStandardPayloadFormComponent extends AbstractForm<
@@ -17,19 +17,12 @@
<button <button
pButton pButton
type="button" type="button"
size="large"
[label]="seasonItem.label" [label]="seasonItem.label"
[severity]="seasonItem.severity" [severity]="seasonItem.severity"
outlined outlined
(click)="selectedSeasonDraft.set(seasonItem.key)"></button> (click)="selectSeason(seasonItem)"></button>
} }
<!-- [disabled]="selectedYear <= selectedYearDraft() && selectedSeason <= selectedSeasonDraft()" --> <!-- [disabled]="selectedYear <= selectedYearDraft() && selectedSeason <= selectedSeasonDraft()" -->
</div> </div>
<div class="mt-4">
<app-form-footer-actions
submitLabel="تایید"
cancelLabel="انصراف"
(onCancel)="visibleChange.emit(false)"
(onSubmit)="onSubmit()" />
</div>
</shared-light-bottomsheet> </shared-light-bottomsheet>
@@ -5,7 +5,6 @@ import { Component, computed, EventEmitter, Input, Output, signal } from '@angul
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ButtonDirective, ButtonSeverity } from 'primeng/button'; import { ButtonDirective, ButtonSeverity } from 'primeng/button';
import { SharedLightBottomsheetComponent } from '../dialog/light-bottomsheet.component'; import { SharedLightBottomsheetComponent } from '../dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '../formFooterActions/form-footer-actions.component';
type SeasonKey = 0 | 1 | 2 | 3; type SeasonKey = 0 | 1 | 2 | 3;
@@ -22,7 +21,6 @@ interface SeasonItem {
CommonModule, CommonModule,
FormsModule, FormsModule,
SharedLightBottomsheetComponent, SharedLightBottomsheetComponent,
FormFooterActionsComponent,
ButtonDirective, ButtonDirective,
UikitLabelComponent, UikitLabelComponent,
], ],
@@ -40,10 +38,11 @@ export class SeasonPickerDialogComponent extends AbstractDialog {
isSeasonDisabled = computed(() => this.selectedYear >= this.selectedYearDraft()); isSeasonDisabled = computed(() => this.selectedYear >= this.selectedYearDraft());
onSubmit(): void { selectSeason(season: SeasonItem) {
this.selectedSeasonDraft.set(season.key);
this.submit.emit({ this.submit.emit({
year: this.selectedYearDraft(), year: this.selectedYearDraft(),
season: this.selectedSeasonDraft(), season: season.key,
}); });
this.close(); this.close();
} }
+7 -2
View File
@@ -1,4 +1,5 @@
import { import {
equalLengthValidator,
fiscalIdValidator, fiscalIdValidator,
mobileValidator, mobileValidator,
password, password,
@@ -70,9 +71,13 @@ export const fieldControl = {
value, value,
isRequired ? [...required(), usernameValidator()] : [usernameValidator()], isRequired ? [...required(), usernameValidator()] : [usernameValidator()],
], ],
economic_code: (value = '', isRequired = true): ControlConfig => [ legal_economic_code: (value = '', isRequired = true): ControlConfig => [
value, value,
isRequired ? required() : [], isRequired ? [...required(), equalLengthValidator(11)] : [equalLengthValidator(11)],
],
individual_economic_code: (value = '', isRequired = true): ControlConfig => [
value,
isRequired ? [...required(), equalLengthValidator(14)] : [equalLengthValidator(14)],
], ],
fiscal_id: (value = '', isRequired = true): ControlConfig => [ fiscal_id: (value = '', isRequired = true): ControlConfig => [
value, value,
@@ -43,7 +43,7 @@
[ngClass]="{ [ngClass]="{
'bg-surface-ground font-bold': day.isCurrentDay, 'bg-surface-ground font-bold': day.isCurrentDay,
'bg-primary! text-primary-contrast!': isSelected(day), 'bg-primary! text-primary-contrast!': isSelected(day),
'text-muted-color! font-normal': day.isDisabled, 'text-muted-color! font-light': day.isDisabled,
}" }"
(click)="selectDate(day)"> (click)="selectDate(day)">
{{ day.day }} {{ day.day }}