Compare commits

..

2 Commits

38 changed files with 293 additions and 134 deletions
@@ -68,6 +68,11 @@ 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['invalidFiscalId']) {
const info = errors['invalidFiscalId'];
out.push({ key: 'invalidFiscalId', message: info.message ?? `${label} معتبر نیست.` });
}
// fallback: include any other error keys // fallback: include any other error keys
Object.keys(errors).forEach((k) => { Object.keys(errors).forEach((k) => {
if ( if (
@@ -80,6 +85,7 @@ export class FormErrorsService {
'max', 'max',
'email', 'email',
'pattern', 'pattern',
'invalidFiscalId',
].indexOf(k) === -1 ].indexOf(k) === -1
) { ) {
try { try {
@@ -1,13 +0,0 @@
import { ValidatorFn } from '@angular/forms';
export function fiscalCodeValidator(): ValidatorFn {
return (control) => {
if (control.value === null || control.value === undefined || control.value === '') {
return null;
}
return null;
return control.value.length === 11 && /^[0-9]{11}$/.test(control.value)
? null
: { fiscalCode: 'معتبر نیست' };
};
}
@@ -0,0 +1,30 @@
import { ValidatorFn } from '@angular/forms';
export function fiscalIdValidator(): ValidatorFn {
return (control) => {
if (control.value === null || control.value === undefined || control.value === '') {
return null;
}
if (control.value.length < 6) {
return {
minlength: {
requiredLength: 6,
actualLength: control.value.length,
},
};
}
const pattern = /^[a-zA-Z0-9]*$/;
if (!pattern.test(control.value)) {
return {
invalidFiscalId: {
value: control.value,
message: 'شناسه مالی فقط می‌تواند شامل حروف انگلیسی و اعداد باشد',
},
};
}
return null;
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
export * from './fiscal-code.validator'; export * from './fiscal-id.validator';
export * from './greater.validator'; export * from './greater.validator';
export * from './iban.validator'; export * from './iban.validator';
export * from './mobile.validator'; export * from './mobile.validator';
+12 -7
View File
@@ -1,16 +1,21 @@
import { ValidatorFn } from '@angular/forms'; import { ValidatorFn } from '@angular/forms';
// Password must be minimum 8 characters, include at least one uppercase, // const PASSWORD_PATTERN = /^[a-zA-Z0-9_-]*$/;
// one lowercase, one number and one special character
// export const PASSWORD_PATTERN = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/;
export const PASSWORD_PATTERN = /^[0-9]{6,}$/;
// Validator factory named `password` as requested. Returns `null` for empty
// values so `Validators.required` can be used alongside it when needed.
export function password(): ValidatorFn { export function password(): ValidatorFn {
return (control) => { return (control) => {
const value = control?.value; const value = control?.value;
if (value === null || value === undefined || String(value).length === 0) return null; if (value === null || value === undefined || String(value).length === 0) return null;
return PASSWORD_PATTERN.test(String(value)) ? null : { password: 'معتبر نیست' };
if (value.length < 6) {
return {
minlength: {
requiredLength: 6,
actualLength: value.length,
},
};
}
return null;
// return PASSWORD_PATTERN.test(String(value)) ? null : { password: 'معتبر نیست' };
}; };
} }
@@ -9,28 +9,28 @@ export const CONSUMER_MENU_ITEMS = [
}, },
{ {
label: 'فعالیت اقتصادی', label: 'فعالیت اقتصادی',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-shop',
routerLink: ['/consumer/business_activities'], routerLink: ['/consumer/business_activities'],
}, },
{ {
label: 'پایانه‌های فروش', label: 'پایانه‌ی فروش',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-tablet',
routerLink: ['/consumer/poses'], routerLink: ['/consumer/poses'],
}, },
{ {
label: 'فاکتورها', label: 'فاکتورها',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-receipt',
routerLink: ['/consumer/sale_invoices'], routerLink: ['/consumer/sale_invoices'],
}, },
{ {
label: 'مشتری‌ها', label: 'مشتری‌ها',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-users',
routerLink: ['/consumer/customers'], routerLink: ['/consumer/customers'],
}, },
{ {
label: 'حساب‌های کاربری', label: 'حساب‌های کاربری',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-user',
routerLink: ['/consumer/accounts'], routerLink: ['/consumer/accounts'],
}, },
], ],
@@ -9,7 +9,7 @@
<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-economic-code [control]="form.controls.economic_code" />
<field-fiscal-code [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" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
@@ -9,17 +9,17 @@ export const PARTNER_MENU_ITEMS = [
}, },
{ {
label: 'حساب‌های کاربری', label: 'حساب‌های کاربری',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-user',
routerLink: ['/partner/accounts'], routerLink: ['/partner/accounts'],
}, },
{ {
label: 'مشتری‌ها', label: 'مشتری‌ها',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-users',
routerLink: ['/partner/consumers'], routerLink: ['/partner/consumers'],
}, },
{ {
label: 'لایسنس‌ها', label: 'لایسنس‌ها',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-key',
routerLink: ['/partner/licenses'], routerLink: ['/partner/licenses'],
}, },
], ],
@@ -1,7 +1,8 @@
<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-economic-code [control]="form.controls.economic_code" />
<field-fiscal-code [control]="form.controls.fiscal_id" /> <field-fiscal-id [control]="form.controls.fiscal_id" />
<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" />
<field-guild-id [control]="form.controls.guild_id" /> <field-guild-id [control]="form.controls.guild_id" />
@@ -7,6 +7,7 @@ import {
NameComponent, NameComponent,
PartnerTokenComponent, PartnerTokenComponent,
} from '@/shared/components'; } from '@/shared/components';
import { InvoiceNumberSequenceComponent } from '@/shared/components/fields/invoice_number_sequence.component';
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/fields'; import { fieldControl } from '@/shared/constants/fields';
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
@@ -28,6 +29,7 @@ import { PartnerConsumerBusinessActivitiesService } from '../../services/busines
LicenseStartsAtComponent, LicenseStartsAtComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
Divider, Divider,
InvoiceNumberSequenceComponent,
], ],
}) })
export class ConsumerBusinessActivitiesFormComponent extends AbstractForm< export class ConsumerBusinessActivitiesFormComponent extends AbstractForm<
@@ -44,6 +46,9 @@ export class ConsumerBusinessActivitiesFormComponent extends AbstractForm<
name: fieldControl.name(this.initialValues?.name || ''), name: fieldControl.name(this.initialValues?.name || ''),
economic_code: fieldControl.economic_code(this.initialValues?.economic_code || ''), economic_code: fieldControl.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(
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 || ''),
license_starts_at: [''], license_starts_at: [''],
@@ -7,6 +7,7 @@ export interface IBusinessActivityRawResponse {
fiscal_id: string; fiscal_id: string;
partner_token: string; partner_token: string;
guild: ISummary; guild: ISummary;
invoice_number_sequence: string;
created_at: string; created_at: string;
license_info: LicenseInfo; license_info: LicenseInfo;
} }
+1
View File
@@ -17,6 +17,7 @@ export interface IGoodRawResponse {
is_default_guild_good: boolean; is_default_guild_good: boolean;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
is_favorite: boolean;
} }
export interface IGoodResponse extends IGoodRawResponse {} export interface IGoodResponse extends IGoodRawResponse {}
@@ -0,0 +1,9 @@
<p-button
type="button"
[icon]="`pi pi-${isFavorite ? 'star-fill' : 'star'}`"
class="shrink-0"
[size]="size"
[loading]="loading()"
[severity]="isFavorite ? 'warn' : 'secondary'"
(click)="toggleFavorite(id, isFavorite)"
></p-button>
@@ -0,0 +1,55 @@
import { Component, inject, Input, signal } from '@angular/core';
import { Button } from 'primeng/button';
import { finalize, Observable } from 'rxjs';
import { PosGoodFavoriteService } from '../services/favorite.service';
import { PosLandingStore } from '../store/main.store';
@Component({
selector: 'pos-good-favorite-cta',
templateUrl: 'favorite-CTA.component.html',
imports: [Button],
})
export class FavoriteCTAComponent {
private readonly favoriteService = inject(PosGoodFavoriteService);
private readonly store = inject(PosLandingStore);
@Input({ required: true }) id!: string;
@Input({ required: true }) isFavorite!: boolean;
@Input() size?: 'small' | 'large';
loading = signal(false);
toggleFavorite(goodId: string, currentState: boolean) {
if (this.loading()) return;
if (currentState) {
this.detachFavorite(goodId);
} else {
this.attachFavorite(goodId);
}
}
attachFavorite(goodId: string) {
this.handleFavoriteRequest(this.favoriteService.attach(goodId), goodId, true);
}
detachFavorite(goodId: string) {
this.handleFavoriteRequest(this.favoriteService.detach(goodId), goodId, false);
}
private handleFavoriteRequest(
request$: Observable<unknown>,
goodId: string,
isFavorite: boolean,
) {
this.loading.set(true);
request$
.pipe(
finalize(() => {
this.loading.set(false);
}),
)
.subscribe(() => {
this.store.toggleGoodFavorite(goodId, isFavorite);
});
}
}
@@ -7,7 +7,7 @@
} }
} @else { } @else {
@for (good of visibleGoods(); track good.id) { @for (good of visibleGoods(); track good.id) {
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs flex flex-col" (click)="addProduct(good)"> <div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs flex flex-col">
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
loading="lazy" loading="lazy"
@@ -21,9 +21,9 @@
<small class="text-xs text-muted-color">*</small> <small class="text-xs text-muted-color">*</small>
} }
</span> </span>
<div class="flex items-center justify-between mt-1 w-full px-2 my-2 shrink-0"> <div class="flex items-center justify-between mt-1 w-full px-2 my-2 shrink-0 gap-1">
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> --> <button pButton type="button" label="انتخاب" class="grow w-full" (click)="addProduct(good)"></button>
<button pButton type="button" icon="pi pi-plus" class="w-full!">انتخاب</button> <pos-good-favorite-cta [isFavorite]="good.is_favorite" [id]="good.id" />
</div> </div>
</div> </div>
</div> </div>
@@ -13,26 +13,30 @@ import { ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton'; import { Skeleton } from 'primeng/skeleton';
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';
@Component({ @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], imports: [ButtonDirective, Skeleton, FavoriteCTAComponent],
}) })
export class PosGoodsGridViewComponent { export class PosGoodsGridViewComponent {
private readonly store = inject(PosLandingStore);
@Output() onAdd = new EventEmitter<IGoodResponse>(); @Output() onAdd = new EventEmitter<IGoodResponse>();
goods = computed(() => this.store.filteredGoods()); private readonly store = inject(PosLandingStore);
private readonly pageSize = 80; private readonly pageSize = 80;
readonly renderLimit = signal(this.pageSize); readonly renderLimit = signal(this.pageSize);
goodPlaceholder = images.placeholders.default;
onFavItemsLoading = signal<string[]>([]);
goods = computed(() => this.store.filteredGoods());
visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit())); visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit()));
hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0)); hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0));
loading = computed(() => this.store.getGoodsLoading()); loading = computed(() => this.store.getGoodsLoading());
goodPlaceholder = images.placeholders.default;
addProduct(good: IGoodResponse) { addProduct(good: IGoodResponse) {
this.onAdd.emit(good); this.onAdd.emit(good);
} }
@@ -7,7 +7,7 @@
} }
} @else { } @else {
@for (good of visibleGoods(); track good.id) { @for (good of visibleGoods(); track good.id) {
<div class="flex items-center bg-surface-card rounded-lg p-1 shadow-xs gap-2"> <div class="flex items-center bg-surface-card rounded-lg py-1 px-3 shadow-xs gap-2">
<div class="grow flex items-center gap-2"> <div class="grow flex items-center gap-2">
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
@@ -16,7 +16,7 @@
class="w-12 h-12 object-cover rounded-md" class="w-12 h-12 object-cover rounded-md"
/> />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-lg 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-xs text-muted-color">*</small> <small class="text-xs text-muted-color">*</small>
@@ -27,8 +27,9 @@
</span> </span>
</div> </div>
</div> </div>
<div class="shrink-0 flex items-center justify-between gap-3"> <div class="shrink-0 flex items-center justify-between gap-1">
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addGood(good)">انتخاب</button> <pos-good-favorite-cta [isFavorite]="good.is_favorite" [id]="good.id" size="small" />
<button pButton type="button" label="انتخاب" class="w-28" size="small" (click)="addGood(good)"></button>
</div> </div>
</div> </div>
} }
@@ -13,26 +13,29 @@ import { ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton'; import { Skeleton } from 'primeng/skeleton';
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';
@Component({ @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], imports: [ButtonDirective, Skeleton, FavoriteCTAComponent],
}) })
export class PosGoodsListViewComponent { export class PosGoodsListViewComponent {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
@Output() onAdd = new EventEmitter<IGoodResponse>(); @Output() onAdd = new EventEmitter<IGoodResponse>();
goods = computed(() => this.store.filteredGoods()); goodPlaceholder = images.placeholders.default;
private readonly pageSize = 120; private readonly pageSize = 120;
readonly renderLimit = signal(this.pageSize); readonly renderLimit = signal(this.pageSize);
goods = computed(() => this.store.filteredGoods());
visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit())); visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit()));
hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0)); hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0));
loading = computed(() => this.store.getGoodsLoading()); loading = computed(() => this.store.getGoodsLoading());
goodPlaceholder = images.placeholders.default;
addGood(good: IGoodResponse) { addGood(good: IGoodResponse) {
this.onAdd.emit(good); this.onAdd.emit(good);
} }
@@ -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-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" type="price" />
<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" />
@@ -31,16 +31,16 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
IPosOrderItem<IGoldPayload>, IPosOrderItem<IGoldPayload>,
IPosOrderItem<IGoldPayload> IPosOrderItem<IGoldPayload>
> { > {
// override defaultValues: Partial<IPosOrderItem<IGoldPayload>> = { override defaultValues: Partial<IPosOrderItem<IGoldPayload>> = {
// unit_price: 200_000_000, // unit_price: 200_000_000,
// quantity: 2, // quantity: 2,
// payload: { payload: {
// // commission: 10_000, // commission: 10_000,
// // wages: 10_000, // wages: 10_000,
// // profit: 10_000, // profit: 10_000,
// karat: 'KARAT_18' as TGoldKarat, karat: 'KARAT_18' as TGoldKarat,
// } as any, } as any,
// }; };
@Input({ required: true }) vatPercentage!: number; @Input({ required: true }) vatPercentage!: number;
@@ -14,7 +14,7 @@
</form> </form>
<div class="form-group"> <div class="form-group">
<uikit-field label="تعداد مراحل پرداخت با پایانه" name="pay_by_terminal_steps"> <uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps">
<p-select <p-select
[options]="payByTerminalSteps()" [options]="payByTerminalSteps()"
[ngModel]="selectedPayByTerminalStep()" [ngModel]="selectedPayByTerminalStep()"
@@ -30,7 +30,7 @@
<app-input <app-input
[control]="terminalControl" [control]="terminalControl"
[name]="'terminal_' + $index" [name]="'terminal_' + $index"
[label]="$index === 0 ? 'پرداخت با پایانه' : 'پرداخت با پایانه - مرحله ' + ($index + 1)" [label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
type="price" type="price"
[max]="terminalsMax()[$index]" [max]="terminalsMax()[$index]"
[disabled]="payByTerminalSteps()[$index].payed" [disabled]="payByTerminalSteps()[$index].payed"
@@ -43,24 +43,30 @@
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed" [disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
(click)="fillTerminalRemained($index)" (click)="fillTerminalRemained($index)"
> >
تمامی بدهی باقی‌مانده باقی‌مانده مبلغ
</button> </button>
</ng-template> </ng-template>
</app-input> </app-input>
} }
</div> </div>
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.cash" name="cash" label="نقدی" type="price" [max]="cashMax()"> <app-input
[control]="form.controls.cash"
name="cash"
label="نقدی/ کارت‌خوان دیگر/ کارت به کارت"
type="price"
[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-input>
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()"> <app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [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-input>
@@ -0,0 +1,5 @@
const baseUrl = (good_id: string) => `/api/v1/pos/goods/${good_id}/favorite`;
export const POS_GOOD_FAVORITE_API_ROUTES = {
favorite: (good_id: string) => `${baseUrl(good_id)}`,
};
@@ -1,3 +1,4 @@
export * from './favorite';
const baseUrl = '/api/v1/pos'; const baseUrl = '/api/v1/pos';
export const POS_API_ROUTES = { export const POS_API_ROUTES = {
@@ -0,0 +1,21 @@
import { IPosInfoRawResponse, IPosInfoResponse } from '@/domains/pos/models/pos.io';
import { IPosProfileRawResponse, IPosProfileResponse } from '@/domains/pos/models/profile.io';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { POS_GOOD_FAVORITE_API_ROUTES } from '../constants';
@Injectable({ providedIn: 'root' })
export class PosGoodFavoriteService {
constructor(private http: HttpClient) {}
private apiRoutes = POS_GOOD_FAVORITE_API_ROUTES;
attach(good_id: string): Observable<IPosProfileResponse> {
return this.http.post<IPosProfileRawResponse>(this.apiRoutes.favorite(good_id), {});
}
detach(good_id: string): Observable<IPosInfoResponse> {
return this.http.delete<IPosInfoRawResponse>(this.apiRoutes.favorite(good_id));
}
}
@@ -5,12 +5,13 @@ import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils'; import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils';
import { computed, inject, Injectable, signal } from '@angular/core'; import { computed, inject, Injectable, signal } from '@angular/core';
import { catchError, finalize, map, throwError } from 'rxjs'; import { catchError, finalize, firstValueFrom, map, throwError } from 'rxjs';
import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models'; import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models';
import { IPosInOrderGood, IPosOrderItem } from '../models/types'; import { IPosInOrderGood, IPosOrderItem } from '../models/types';
import { PosService } from '../services/main.service'; import { PosService } from '../services/main.service';
export type TViewType = 'list' | 'grid'; export type TViewType = 'list' | 'grid';
const FAVORITE_CATEGORY_ID = 'favorite';
interface IPosLandingState { interface IPosLandingState {
getGoodsLoading: boolean; getGoodsLoading: boolean;
@@ -74,6 +75,9 @@ export class PosLandingStore {
if (!this.activeGoodCategory()) { if (!this.activeGoodCategory()) {
return this.state$().goods; return this.state$().goods;
} }
if (this.activeGoodCategory() === FAVORITE_CATEGORY_ID) {
return this.state$().goods?.filter((good) => good.is_favorite);
}
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory); return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
}); });
readonly filteredGoods = computed(() => readonly filteredGoods = computed(() =>
@@ -115,55 +119,38 @@ export class PosLandingStore {
this.state$.set({ ...INITIAL_POS_STATE }); this.state$.set({ ...INITIAL_POS_STATE });
} }
initial() { async initial() {
this.getGoods(); await Promise.all([this.getGoods(), this.getGoodCategories()]);
this.getGoodCategories();
this.prepareGoodCategories();
} }
fillInitial(data: Partial<IPosLandingState>) { fillInitial(data: Partial<IPosLandingState>) {
this.state$.set({ ...INITIAL_POS_STATE, ...data }); this.state$.set({ ...INITIAL_POS_STATE, ...data });
} }
getGoods() { private async getGoods() {
this.setState({ getGoodsLoading: true }); this.setState({ getGoodsLoading: true });
this.service.getGoods(this.state$().searchQuery).subscribe({ try {
next: (res) => { const res = await firstValueFrom(this.service.getGoods(this.state$().searchQuery));
this.setState({ goods: res.data, getGoodsLoading: false }); this.setState({ goods: res.data, getGoodsLoading: false });
}, } catch {
error: () => {
this.setState({ getGoodsLoading: false }); this.setState({ getGoodsLoading: false });
}, }
});
} }
getGoodCategories() { private async getGoodCategories() {
this.setState({ getGoodCategoriesLoading: true }); this.setState({ getGoodCategoriesLoading: true });
this.service try {
.getGoodCategories() const res = await firstValueFrom(this.service.getGoodCategories());
.pipe(
map((res) => {
return [
{
id: '',
name: 'همه',
goods_count: res.data.reduce((acc, curr) => acc + curr.goods_count, 0),
},
...res.data,
];
}),
)
.subscribe({
next: (res) => {
this.setState({ this.setState({
goodCategories: res, goodCategories: res.data,
getGoodCategoriesLoading: false, getGoodCategoriesLoading: false,
activeGoodCategory: res[0]?.id,
}); });
}, } catch {
error: () => {
this.setState({ getGoodCategoriesLoading: false }); this.setState({ getGoodCategoriesLoading: false });
}, }
});
} }
changeActiveCategory(categoryId: string) { changeActiveCategory(categoryId: string) {
@@ -210,6 +197,46 @@ export class PosLandingStore {
this.setState({ searchQuery }); this.setState({ searchQuery });
} }
toggleGoodFavorite(goodId: string, newState: boolean) {
const updatedGoods = this.state$().goods?.map((good) => {
if (good.id === goodId) {
return { ...good, is_favorite: newState };
}
return good;
});
this.setState({ goods: updatedGoods || this.state$().goods });
this.prepareGoodCategories();
}
private prepareGoodCategories() {
const rawCategories =
this.goodCategories()?.filter(({ id }) => id !== '' && id !== FAVORITE_CATEGORY_ID) || [];
const favoriteGoodsCount = this.goods()?.filter((good) => good.is_favorite).length || 0;
const goodsCount = this.goods()?.length || 0;
const preparedGoodCategories = rawCategories;
if (favoriteGoodsCount > 0) {
preparedGoodCategories.unshift({
id: FAVORITE_CATEGORY_ID,
name: 'علاقه‌مندی‌ها',
goods_count: favoriteGoodsCount,
});
}
preparedGoodCategories.unshift({
id: '',
name: 'همه',
goods_count: goodsCount,
});
this.setState({
goodCategories: preparedGoodCategories,
});
if (!this.activeGoodCategory()) {
this.setState({ activeGoodCategory: '' });
}
}
filterGoods() {} filterGoods() {}
resetInOrderGoods() { resetInOrderGoods() {
@@ -13,5 +13,6 @@ export class LayoutComponent {
ngOnInit() { ngOnInit() {
this.layoutService.setMenuItems(SUPER_ADMIN_MENU_ITEMS); this.layoutService.setMenuItems(SUPER_ADMIN_MENU_ITEMS);
this.layoutService.setPanelInfo({ title: 'پنل مدیریتی ' });
} }
} }
@@ -9,7 +9,7 @@
<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-economic-code [control]="form.controls.economic_code" />
<field-fiscal-code [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" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
@@ -6,8 +6,8 @@
</ng-template> </ng-template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="listKeyValue"> <div class="listKeyValue">
<app-key-value label="عنوان شریک تجاری" [value]="partner()?.name" /> <app-key-value label="عنوان" [value]="partner()?.name" />
<app-key-value label="کد شریک تجاری" [value]="partner()?.code" /> <app-key-value label="کد" [value]="partner()?.code" />
<div class="col-span-3 grid grid-cols-3 gap-4 mt-6"> <div class="col-span-3 grid grid-cols-3 gap-4 mt-6">
<partner-license-info-template <partner-license-info-template
@@ -2,7 +2,7 @@
<p-select <p-select
[loading]="loading()" [loading]="loading()"
[options]="items()" [options]="items()"
optionLabel="name" optionLabel="fullname"
[optionValue]="selectOptionValue" [optionValue]="selectOptionValue"
placeholder="انتخاب شناسه کالا" placeholder="انتخاب شناسه کالا"
[formControl]="control" [formControl]="control"
@@ -3,11 +3,12 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from '../input/input.component'; import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-fiscal-code', selector: 'field-fiscal-id',
template: `<app-input template: `<app-input
label="کد مالیاتی" label="شناسه یکتا"
[control]="control" [control]="control"
[name]="name" [name]="name"
[length]="6"
[isLtrInput]="true" [isLtrInput]="true"
/>`, />`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
@@ -10,6 +10,7 @@ import { InputComponent } from '../input/input.component';
[name]="name" [name]="name"
type="number" type="number"
[min]="min" [min]="min"
[maxLength]="9"
/>`, />`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
@@ -4,12 +4,7 @@ import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-partner-token', selector: 'field-partner-token',
template: `<app-input template: `<app-input label="توکن" [control]="control" [name]="name" [isLtrInput]="true" />`,
label="توکن پارتنر"
[control]="control"
[name]="name"
[isLtrInput]="true"
/>`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
export class PartnerTokenComponent { export class PartnerTokenComponent {
@@ -4,7 +4,7 @@ import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-terminal', selector: 'field-terminal',
template: `<app-input label="پرداخت با پایانه" [control]="control" [name]="name" type="price" />`, template: `<app-input label="پرداخت با پوز" [control]="control" [name]="name" type="price" />`,
imports: [ReactiveFormsModule, InputComponent], imports: [ReactiveFormsModule, InputComponent],
}) })
export class TerminalComponent { export class TerminalComponent {
@@ -66,6 +66,7 @@ export class InputComponent {
@Input() isLtrInput = false; @Input() isLtrInput = false;
@Input() suffix: string = ''; @Input() suffix: string = '';
@Input() maxLength?: number; @Input() maxLength?: number;
@Input() length?: number;
@Input() min?: number; @Input() min?: number;
@Input() max?: number; @Input() max?: number;
@Input() fixed?: number; @Input() fixed?: number;
@@ -132,6 +133,8 @@ export class InputComponent {
} }
get preparedMaxLength(): number | null { get preparedMaxLength(): number | null {
const length = this.length || this.maxLength;
if (length) return length;
switch (this.type) { switch (this.type) {
case 'mobile': case 'mobile':
return 11; return 11;
@@ -33,7 +33,7 @@
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" /> <app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
@for (payment of invoice.payments; track $index) { @for (payment of invoice.payments; track $index) {
<app-key-value <app-key-value
[label]="`${payment.payment_method === 'SET_OFF' ? 'تهاتر' : payment.payment_method === 'TERMINAL' ? 'پایانه' : 'نقدی'}`" [label]="`${payment.payment_method === 'SET_OFF' ? 'تهاتر' : payment.payment_method === 'TERMINAL' ? 'پوز' : 'نقدی/ کارت‌خوان دیگر/ کارت به کارت'}`"
[value]="payment.amount" [value]="payment.amount"
type="price" type="price"
/> />
@@ -11,8 +11,7 @@ import { Password } from 'primeng/password';
export class SharedPasswordInputComponent { export class SharedPasswordInputComponent {
@Input({ required: true }) passwordControl = new FormControl<string>(''); @Input({ required: true }) passwordControl = new FormControl<string>('');
@Input({ required: true }) confirmPasswordControl = new FormControl<string>(''); @Input({ required: true }) confirmPasswordControl = new FormControl<string>('');
@Input() hint: string = @Input() hint: string = 'رمز باید حداقل ۶ کاراکتر باشد.';
'رمز باید حداقل ۸ کاراکتر بوده و شامل حداقل یک حرف بزرگ، یک حرف کوچک، یک عدد و یک کاراکتر ویژه باشد.';
@Input() fluid: boolean = true; @Input() fluid: boolean = true;
@Input() size: 'small' | 'large' = 'large'; @Input() size: 'small' | 'large' = 'large';
} }
+2 -11
View File
@@ -1,5 +1,5 @@
import { import {
fiscalCodeValidator, fiscalIdValidator,
mobileValidator, mobileValidator,
password, password,
postalCodeValidator, postalCodeValidator,
@@ -30,15 +30,10 @@ export const fieldControl = {
value, value,
isRequired ? required() : [], isRequired ? required() : [],
], ],
firstName: (value = '', isRequired = true): ControlConfig => [
value,
isRequired ? required() : [],
],
last_name: (value = '', isRequired = true): ControlConfig => [ last_name: (value = '', isRequired = true): ControlConfig => [
value, value,
isRequired ? required() : [], isRequired ? required() : [],
], ],
lastName: (value = '', isRequired = true): ControlConfig => [value, isRequired ? required() : []],
legal_name: (value = '', isRequired = true): ControlConfig => [ legal_name: (value = '', isRequired = true): ControlConfig => [
value, value,
isRequired ? required() : [], isRequired ? required() : [],
@@ -55,10 +50,6 @@ export const fieldControl = {
value, value,
isRequired ? [Validators.required, mobileValidator()] : [mobileValidator()], isRequired ? [Validators.required, mobileValidator()] : [mobileValidator()],
], ],
mobile: (value = '', isRequired = true): ControlConfig => [
value,
isRequired ? [Validators.required, mobileValidator()] : [mobileValidator()],
],
email: (value = '', isRequired = false): ControlConfig => [ email: (value = '', isRequired = false): ControlConfig => [
value, value,
isRequired ? [Validators.required, Validators.email] : [Validators.email], isRequired ? [Validators.required, Validators.email] : [Validators.email],
@@ -85,7 +76,7 @@ export const fieldControl = {
], ],
fiscal_id: (value = '', isRequired = true): ControlConfig => [ fiscal_id: (value = '', isRequired = true): ControlConfig => [
value, value,
isRequired ? [Validators.required, fiscalCodeValidator()] : [fiscalCodeValidator()], isRequired ? [Validators.required, fiscalIdValidator()] : [fiscalIdValidator()],
], ],
partner_token: (value = '', isRequired = true): ControlConfig => [ partner_token: (value = '', isRequired = true): ControlConfig => [
value, value,
+2 -2
View File
@@ -1,8 +1,8 @@
// TIS tenant environment configuration // TIS tenant environment configuration
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: 'https://psp-api.shift-am.ir', // apiBaseUrl: 'https://psp-api.shift-am.ir',
// apiBaseUrl: 'http://192.168.128.73:5002', apiBaseUrl: 'http://192.168.128.73:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,
enableLogging: false, enableLogging: false,