update pos consumer
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"@primeng/themes": "^20.2.0",
|
"@primeng/themes": "^20.2.0",
|
||||||
"@primeuix/themes": "^1.2.1",
|
"@primeuix/themes": "^1.2.1",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
"@zoomit/dayjs-jalali-plugin": "^0.1.11",
|
||||||
"chart.js": "4.4.2",
|
"chart.js": "4.4.2",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
|
|||||||
Generated
+272
-264
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -19,13 +19,13 @@ export const appRoutes: Routes = [
|
|||||||
CONSUMER_ROUTES,
|
CONSUMER_ROUTES,
|
||||||
PROVIDER_ROUTES,
|
PROVIDER_ROUTES,
|
||||||
PARTNER_ROUTES,
|
PARTNER_ROUTES,
|
||||||
POS_ROUTES,
|
|
||||||
{ path: 'ng', component: Dashboard },
|
{ path: 'ng', component: Dashboard },
|
||||||
{ path: 'uikit', loadChildren: () => import('./app/pages/uikit/uikit.routes') },
|
{ path: 'uikit', loadChildren: () => import('./app/pages/uikit/uikit.routes') },
|
||||||
{ path: 'documentation', component: Documentation },
|
{ path: 'documentation', component: Documentation },
|
||||||
{ path: 'pages', loadChildren: () => import('./app/pages/pages.routes') },
|
{ path: 'pages', loadChildren: () => import('./app/pages/pages.routes') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
POS_ROUTES,
|
||||||
// {
|
// {
|
||||||
// path: 'pos/:posId',
|
// path: 'pos/:posId',
|
||||||
// component: POSComponent,
|
// component: POSComponent,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { environment } from 'src/environments/environment';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiBaseUrlInterceptor implements HttpInterceptor {
|
export class ApiBaseUrlInterceptor implements HttpInterceptor {
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
console.log('first');
|
|
||||||
|
|
||||||
const cookieService = inject(CookieService);
|
const cookieService = inject(CookieService);
|
||||||
// Prepare default headers but don't overwrite existing ones
|
// Prepare default headers but don't overwrite existing ones
|
||||||
const defaultHeaders: Record<string, string> = {
|
const defaultHeaders: Record<string, string> = {
|
||||||
@@ -33,12 +31,13 @@ export class ApiBaseUrlInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('req.url');
|
|
||||||
console.log(req.url);
|
|
||||||
|
|
||||||
// Only prepend base URL if the request URL is relative (does not start with http or https)
|
// Only prepend base URL if the request URL is relative (does not start with http or https)
|
||||||
if (!/^https?:\/\//i.test(req.url)) {
|
if (!/^https?:\/\//i.test(req.url)) {
|
||||||
const apiReq = req.clone({ url: environment.apiBaseUrl + req.url, setHeaders: headersToSet });
|
const apiReq = req.clone({
|
||||||
|
url: environment.apiBaseUrl + req.url,
|
||||||
|
setHeaders: headersToSet,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
return next.handle(apiReq);
|
return next.handle(apiReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { AuthService } from '../services/auth.service';
|
|||||||
* - Prevents duplicate refresh token requests
|
* - Prevents duplicate refresh token requests
|
||||||
*/
|
*/
|
||||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
console.log('authInterceptor');
|
||||||
|
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
|
||||||
// Skip auth for certain endpoints
|
// Skip auth for certain endpoints
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
export function nationalIdValidator(): ValidatorFn {
|
||||||
|
return (control) => {
|
||||||
|
if (control.value === null || control.value === undefined || control.value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return control.value.length === 10 && /^[0-9]{10}$/.test(control.value)
|
||||||
|
? null
|
||||||
|
: { nationalId: true };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<shared-page-loading />
|
<shared-page-loading />
|
||||||
} @else {
|
} @else {
|
||||||
|
<div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4">
|
||||||
|
<div class="w-full flex items-center gap-4 shrink-0 justify-between">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="w-10 h-10">
|
||||||
|
<img [src]="placeholderLogo" alt="Logo" class="w-full h-full object-cover rounded-md bg-surface-card" />
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-bold">{{ posInfo()?.name }} - فروشگاه {{ posInfo()?.complex?.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
|
||||||
|
<span [jalaliDate]="now" jalaliFormat="dddd YYYY/MM/DD" class="text-base"></span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
|
||||||
|
<span class="text-base"> {{ userInfo()?.username }} </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
|
import { AuthService } from '@/core';
|
||||||
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
||||||
|
import { JalaliDateDirective } from '@/shared/directives';
|
||||||
import { Component, computed, inject } from '@angular/core';
|
import { Component, computed, inject } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import images from 'src/assets/images';
|
||||||
import { PosStore } from '../pos.store';
|
import { PosStore } from '../pos.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-layout',
|
selector: 'pos-layout',
|
||||||
templateUrl: './layout.component.html',
|
templateUrl: './layout.component.html',
|
||||||
imports: [PageLoadingComponent, RouterOutlet],
|
imports: [PageLoadingComponent, RouterOutlet, JalaliDateDirective],
|
||||||
})
|
})
|
||||||
export class PosLayoutComponent {
|
export class PosLayoutComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
private readonly store = inject(PosStore);
|
private readonly store = inject(PosStore);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
|
||||||
readonly loading = computed(() => this.store.loading());
|
readonly loading = computed(() => this.store.loading());
|
||||||
readonly posInfo = computed(() => this.store.entity());
|
readonly posInfo = computed(() => this.store.entity());
|
||||||
|
readonly userInfo = computed(() => this.authService.currentAccount());
|
||||||
|
|
||||||
|
placeholderLogo = images.placeholders.logo;
|
||||||
|
|
||||||
|
now = new Date();
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
this.store.getData();
|
this.store.getData();
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import ISummary from '@/core/models/summary';
|
||||||
|
import { UnitType } from '@/utils';
|
||||||
|
|
||||||
|
export interface IGoodRawResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sku: string;
|
||||||
|
category: ISummary;
|
||||||
|
unit_type: UnitType;
|
||||||
|
pricing_model: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
export interface IGoodResponse extends IGoodRawResponse {}
|
||||||
|
|
||||||
|
export interface IGoodCategoryRawResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
goods_count: number;
|
||||||
|
}
|
||||||
|
export interface IGoodCategoryResponse extends IGoodCategoryRawResponse {}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="flex gap-3 overflow-auto">
|
||||||
|
@if (loading()) {
|
||||||
|
@for (i of [1, 2, 3, 4, 5]; track i) {
|
||||||
|
<p-skeleton width="6rem" height="2.5rem" />
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@for (category of categories(); track category.id) {
|
||||||
|
<div
|
||||||
|
[class]="
|
||||||
|
'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all' +
|
||||||
|
(selectedCategory() === category.id ? ' bg-surface-card' : '')
|
||||||
|
"
|
||||||
|
(click)="changeSelectedCategory(category)"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-muted-color font-bold"> {{ category.name }}</span>
|
||||||
|
<div
|
||||||
|
[class]="
|
||||||
|
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' +
|
||||||
|
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ category.goods_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||||
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-good-categories',
|
||||||
|
templateUrl: './categories.component.html',
|
||||||
|
imports: [Skeleton],
|
||||||
|
})
|
||||||
|
export class PosGoodCategoriesComponent {
|
||||||
|
@Output() categoryChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
categories = this.store.goodCategories;
|
||||||
|
loading = this.store.getGoodCategoriesLoading;
|
||||||
|
|
||||||
|
selectedCategory = this.store.activeGoodCategory;
|
||||||
|
|
||||||
|
changeSelectedCategory(category: { id: string; name: string }) {
|
||||||
|
this.store.changeActiveCategory(category.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<p-dialog [(visible)]="visible" header="اطلاعات مشتری" [modal]="true" [style]="{ width: '560px' }">
|
||||||
|
<div class="card p-2! flex justify-center">
|
||||||
|
<p-selectbutton [options]="customerTypes" [(ngModel)]="selectedCustomerType" optionValue="value">
|
||||||
|
<ng-template #item let-item>
|
||||||
|
<span class="text-sm">{{ item.label }}</span>
|
||||||
|
</ng-template>
|
||||||
|
</p-selectbutton>
|
||||||
|
</div>
|
||||||
|
@if (selectedCustomerType() === "UNKNOWN") {
|
||||||
|
<pos-customer-unknown-form (onSubmit)="onSubmitCustomer()" (onClose)="close()" />
|
||||||
|
} @else if (selectedCustomerType() === "INDIVIDUAL") {
|
||||||
|
<pos-customer-individual-form (onSubmit)="onSubmitCustomer()" (onClose)="close()" />
|
||||||
|
} @else if (selectedCustomerType() === "LEGAL") {
|
||||||
|
<pos-customer-legal-form (onSubmit)="onSubmitCustomer()" (onClose)="close()" />
|
||||||
|
}
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||||
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { Component, EventEmitter, inject, Output, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Dialog } from 'primeng/dialog';
|
||||||
|
import { SelectButton } from 'primeng/selectbutton';
|
||||||
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
import { CustomerIndividualFormComponent } from './individual/form.component';
|
||||||
|
import { CustomerLegalFormComponent } from './legal/form.component';
|
||||||
|
import { CustomerUnknownFormComponent } from './unknown/form.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-order-customer-dialog',
|
||||||
|
templateUrl: './customer-dialog.component.html',
|
||||||
|
imports: [
|
||||||
|
Dialog,
|
||||||
|
CustomerIndividualFormComponent,
|
||||||
|
FormsModule,
|
||||||
|
CustomerLegalFormComponent,
|
||||||
|
SelectButton,
|
||||||
|
CustomerUnknownFormComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PosOrderCustomerDialogComponent extends AbstractDialog {
|
||||||
|
@Output() onSubmit = new EventEmitter<any>();
|
||||||
|
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
customerTypes = [
|
||||||
|
{
|
||||||
|
label: 'بدون اطلاعات خریدار (نوع دوم)',
|
||||||
|
value: 'UNKNOWN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'خریدار حقیقی (نوع اول)',
|
||||||
|
value: 'INDIVIDUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'خریدار حقوقی (نوع اول)',
|
||||||
|
value: 'LEGAL',
|
||||||
|
},
|
||||||
|
] as {
|
||||||
|
label: string;
|
||||||
|
value: CustomerType;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
selectedCustomerType = signal<CustomerType>(this.store.customer().type);
|
||||||
|
|
||||||
|
onSubmitCustomer() {
|
||||||
|
this.close();
|
||||||
|
this.onSubmit.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<app-input [control]="form.controls.first_name" name="first_name" label="نام" />
|
||||||
|
<app-input [control]="form.controls.last_name" name="last_name" label="نام خانوادگی" />
|
||||||
|
<app-input [control]="form.controls.national_id" name="national_id" label="کد ملی" type="nationalId" />
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.economic_code"
|
||||||
|
name="economic_code"
|
||||||
|
label="کد اقتصادی"
|
||||||
|
type="number"
|
||||||
|
maxlength="11"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { postalCodeValidator } from '@/core/validators';
|
||||||
|
import { nationalIdValidator } from '@/core/validators/national-id.validator';
|
||||||
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IIndividualCustomer } from '../../../models/customer';
|
||||||
|
import { PosLandingStore } from '../../../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-customer-individual-form',
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent],
|
||||||
|
})
|
||||||
|
export class CustomerIndividualFormComponent extends AbstractForm<
|
||||||
|
IIndividualCustomer,
|
||||||
|
IIndividualCustomer
|
||||||
|
> {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
override showSuccessMessage = false;
|
||||||
|
form = this.fb.group({
|
||||||
|
first_name: [
|
||||||
|
this.store.customer().info?.customerIndividuals?.first_name || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
last_name: [
|
||||||
|
this.store.customer().info?.customerIndividuals?.last_name || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
national_id: [
|
||||||
|
this.store.customer().info?.customerIndividuals?.national_id || '',
|
||||||
|
[Validators.required, nationalIdValidator()],
|
||||||
|
],
|
||||||
|
economic_code: [
|
||||||
|
this.store.customer().info?.customerIndividuals?.economic_code || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
postal_code: [
|
||||||
|
this.store.customer().info?.customerIndividuals?.postal_code || '',
|
||||||
|
[postalCodeValidator()],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
override submitForm() {
|
||||||
|
return new Observable<IIndividualCustomer>((observer) => {
|
||||||
|
const info = this.form.value as IIndividualCustomer;
|
||||||
|
this.store.setCustomer({
|
||||||
|
customer_id: '',
|
||||||
|
type: CustomerType.INDIVIDUAL,
|
||||||
|
info: {
|
||||||
|
customerIndividuals: info,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return observer.next(info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<app-input [control]="form.controls.company_name" name="company_name" label="نام مجموعه" />
|
||||||
|
<app-input [control]="form.controls.registration_number" name="registration_number" label="شماره ثبت" />
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.economic_code"
|
||||||
|
name="economic_code"
|
||||||
|
label="کد اقتصادی"
|
||||||
|
type="number"
|
||||||
|
maxlength="11"
|
||||||
|
/>
|
||||||
|
<app-input [control]="form.controls.postal_code" name="postal_code" label="کد پستی" type="postalCode" />
|
||||||
|
|
||||||
|
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" (onSubmit)="submit()" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { postalCodeValidator } from '@/core/validators';
|
||||||
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ILegalCustomer } from '../../../models/customer';
|
||||||
|
import { PosLandingStore } from '../../../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-customer-legal-form',
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent],
|
||||||
|
})
|
||||||
|
export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILegalCustomer> {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
override showSuccessMessage = false;
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
company_name: [
|
||||||
|
this.store.customer().info?.customerLegals?.company_name || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
registration_number: [
|
||||||
|
this.store.customer().info?.customerLegals?.registration_number || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
economic_code: [
|
||||||
|
this.store.customer().info?.customerLegals?.economic_code || '',
|
||||||
|
[Validators.required],
|
||||||
|
],
|
||||||
|
postal_code: [
|
||||||
|
this.store.customer().info?.customerLegals?.postal_code || '',
|
||||||
|
[postalCodeValidator()],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
override submitForm() {
|
||||||
|
return new Observable<ILegalCustomer>((observer) => {
|
||||||
|
const info = this.form.value as ILegalCustomer;
|
||||||
|
this.store.setCustomer({
|
||||||
|
customer_id: '',
|
||||||
|
type: CustomerType.LEGAL,
|
||||||
|
info: {
|
||||||
|
customerLegals: info,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return observer.next(info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" />
|
||||||
|
<app-input [control]="form.controls.name" name="name" label="نام" />
|
||||||
|
<app-form-footer-actions submitLabel="ثبت بدون اطلاعات خریدار و ادامه" (onCancel)="close()" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { nationalIdValidator } from '@/core/validators/national-id.validator';
|
||||||
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IUnknownCustomer } from '../../../models/customer';
|
||||||
|
import { PosLandingStore } from '../../../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-customer-unknown-form',
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
imports: [ReactiveFormsModule, InputComponent, FormFooterActionsComponent],
|
||||||
|
})
|
||||||
|
export class CustomerUnknownFormComponent extends AbstractForm<IUnknownCustomer, IUnknownCustomer> {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
override showSuccessMessage = false;
|
||||||
|
form = this.fb.group({
|
||||||
|
national_id: [
|
||||||
|
this.store?.customer().info?.customerUnknown?.national_id || '',
|
||||||
|
[nationalIdValidator()],
|
||||||
|
],
|
||||||
|
name: [this.store?.customer().info?.customerUnknown?.name || ''],
|
||||||
|
});
|
||||||
|
|
||||||
|
override submitForm() {
|
||||||
|
return new Observable<IUnknownCustomer>((observer) => {
|
||||||
|
const info = this.form.value as IUnknownCustomer;
|
||||||
|
this.store.setCustomer({
|
||||||
|
customer_id: '',
|
||||||
|
type: CustomerType.UNKNOWN,
|
||||||
|
info: {
|
||||||
|
customerUnknown: info,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return observer.next(info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="sticky top-0 bg-surface-ground z-10 pb-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2 text-muted-color">
|
||||||
|
<i class="pi pi-list"></i>
|
||||||
|
<span class="text-base font-bold">لیست کالاها</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<app-search-input (search)="onSearch($event)"></app-search-input>
|
||||||
|
|
||||||
|
<div class="card p-2! flex justify-center">
|
||||||
|
<p-selectbutton [options]="viewOptions" [(ngModel)]="viewType" optionValue="value">
|
||||||
|
<ng-template #item let-item>
|
||||||
|
<i [class]="item.icon"></i>
|
||||||
|
</ng-template>
|
||||||
|
</p-selectbutton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pos-good-categories (categoryChange)="onCategoryChange($event)" />
|
||||||
|
</div>
|
||||||
|
@if (viewType === "grid") {
|
||||||
|
<pos-good-grid-view (onAdd)="addGood($event)" />
|
||||||
|
} @else {
|
||||||
|
<pos-goods-list-view (onAdd)="addGood($event)" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (selectedGoodToAdd()) {
|
||||||
|
<pos-payload-form-dialog
|
||||||
|
[(visible)]="showPayloadForm"
|
||||||
|
[good]="selectedGoodToAdd()!"
|
||||||
|
(onClose)="onClosePayloadForm()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { SearchInputComponent } from '@/shared/components/search/search-input.component';
|
||||||
|
import { Component, computed, inject, Input, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SelectButton } from 'primeng/selectbutton';
|
||||||
|
import { PosLandingStore, TViewType } from '../store/main.store';
|
||||||
|
import { PosGoodCategoriesComponent } from './categories.component';
|
||||||
|
import { PosGoodsGridViewComponent } from './grid-view.component';
|
||||||
|
import { PosGoodsListViewComponent } from './list-view.component';
|
||||||
|
import { PayloadFormDialogComponent } from './payloads';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-goods',
|
||||||
|
templateUrl: './goods.component.html',
|
||||||
|
imports: [
|
||||||
|
PosGoodCategoriesComponent,
|
||||||
|
PosGoodsListViewComponent,
|
||||||
|
SelectButton,
|
||||||
|
FormsModule,
|
||||||
|
PosGoodsGridViewComponent,
|
||||||
|
SearchInputComponent,
|
||||||
|
PayloadFormDialogComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PosGoodsComponent {
|
||||||
|
@Input() activeCategory = signal<Maybe<string>>(null);
|
||||||
|
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.store.initial();
|
||||||
|
}
|
||||||
|
|
||||||
|
viewOptions = [
|
||||||
|
{ value: 'list', icon: 'pi pi-list' },
|
||||||
|
{ value: 'grid', icon: 'pi pi-th-large' },
|
||||||
|
];
|
||||||
|
|
||||||
|
showPayloadForm = signal(false);
|
||||||
|
selectedGoodToAdd = signal<Maybe<IGoodResponse>>(null);
|
||||||
|
|
||||||
|
readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryGoods());
|
||||||
|
readonly stock = computed(() => this.store.goods());
|
||||||
|
|
||||||
|
get viewType() {
|
||||||
|
return this.store.viewType();
|
||||||
|
}
|
||||||
|
set viewType(val: TViewType) {
|
||||||
|
this.store.updateViewType(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(searchTerm: string) {
|
||||||
|
this.store.updateSearchQuery(searchTerm);
|
||||||
|
this.store.getGoods();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCategoryChange(categoryId: string) {
|
||||||
|
this.store.changeActiveCategory(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
addGood(good: IGoodResponse) {
|
||||||
|
this.showPayloadForm.set(true);
|
||||||
|
|
||||||
|
this.selectedGoodToAdd.set(good);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosePayloadForm() {
|
||||||
|
this.selectedGoodToAdd.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="grid 2xl:grid-cols-8 xl:grid-cols-6 lg:grid-cols-4 grid-cols-2 gap-3 flex-wrap">
|
||||||
|
@if (loading()) {
|
||||||
|
@for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) {
|
||||||
|
<div class="flex-1 aspect-[0.9]">
|
||||||
|
<p-skeleton width="100%" height="100%"></p-skeleton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@for (good of goods(); track good.id) {
|
||||||
|
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs">
|
||||||
|
<img [src]="goodPlaceholder" class="w-full aspect-[1.3] object-cover rounded-md" />
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-lg font-bold">
|
||||||
|
{{ good.name }}
|
||||||
|
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center justify-between mt-1">
|
||||||
|
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
||||||
|
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addProduct(good)"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { Component, computed, EventEmitter, inject, Output } from '@angular/core';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
|
import images from 'src/assets/images';
|
||||||
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-good-grid-view',
|
||||||
|
templateUrl: './grid-view.component.html',
|
||||||
|
imports: [ButtonDirective, Skeleton],
|
||||||
|
})
|
||||||
|
export class PosGoodsGridViewComponent {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||||
|
|
||||||
|
goods = computed(() => this.store.activatedCategoryGoods());
|
||||||
|
loading = computed(() => this.store.getGoodsLoading());
|
||||||
|
|
||||||
|
goodPlaceholder = images.placeholders.default;
|
||||||
|
|
||||||
|
addProduct(good: IGoodResponse) {
|
||||||
|
this.onAdd.emit(good);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="flex flex-col gap-3 flex-wrap">
|
||||||
|
@if (loading()) {
|
||||||
|
@for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) {
|
||||||
|
<div class="w-100 h-12">
|
||||||
|
<p-skeleton width="100%" height="100%"></p-skeleton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@for (good of goods(); track good.id) {
|
||||||
|
<div class="flex items-center bg-surface-card rounded-lg p-1 shadow-xs gap-2">
|
||||||
|
<div class="grow flex items-center gap-2">
|
||||||
|
<img [src]="goodPlaceholder" class="w-12 h-12 object-cover rounded-md" />
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-lg font-bold">
|
||||||
|
{{ good.name }}
|
||||||
|
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-muted-color">
|
||||||
|
{{ good.sku }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 flex items-center justify-between gap-3">
|
||||||
|
<!-- <span [appPriceMask]="good.salePrice" class="text-base font-bold"></span> -->
|
||||||
|
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addGood(good)"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { Component, computed, EventEmitter, inject, Output } from '@angular/core';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
|
import images from 'src/assets/images';
|
||||||
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-goods-list-view',
|
||||||
|
templateUrl: './list-view.component.html',
|
||||||
|
imports: [ButtonDirective, Skeleton],
|
||||||
|
})
|
||||||
|
export class PosGoodsListViewComponent {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||||
|
|
||||||
|
goods = computed(() => this.store.activatedCategoryGoods());
|
||||||
|
loading = computed(() => this.store.getGoodsLoading());
|
||||||
|
|
||||||
|
goodPlaceholder = images.placeholders.default;
|
||||||
|
|
||||||
|
addGood(good: IGoodResponse) {
|
||||||
|
this.onAdd.emit(good);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="flex items-center gap-2 justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold text-lg">سفارش #{{ heldOrder.orderNumber }}</span>
|
||||||
|
<span class="text-sm text-muted-color">
|
||||||
|
{{ heldOrder.orderItems.length }} کالا -
|
||||||
|
<span [appPriceMask]="heldOrder.totalAmount"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
label="بارگذاری"
|
||||||
|
icon="pi pi-download"
|
||||||
|
(click)="loadHeldOrder(heldOrder.id)"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
label="حذف"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
(click)="cancelHeldOrder(heldOrder.id)"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { IPosHeldOrderResponse } from '@/modules/pos/models';
|
||||||
|
import { ConfirmationDialogService } from '@/shared/components/confirmationDialog/confirmation-dialog.service';
|
||||||
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { PosService } from '../../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-held-order-item',
|
||||||
|
templateUrl: './held-order-item.component.html',
|
||||||
|
imports: [PriceMaskDirective, ButtonDirective],
|
||||||
|
})
|
||||||
|
export class HeldOrderItemComponent {
|
||||||
|
@Input() heldOrder!: IPosHeldOrderResponse;
|
||||||
|
@Input() posId!: number;
|
||||||
|
|
||||||
|
@Output() onCancel = new EventEmitter<number>();
|
||||||
|
@Output() onLoad = new EventEmitter<number>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly posService: PosService,
|
||||||
|
private readonly confirmationService: ConfirmationDialogService,
|
||||||
|
private readonly toastService: ToastService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
actionLoading = signal<boolean>(false);
|
||||||
|
|
||||||
|
cancelHeldOrder(heldOrderId: number) {
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
message: `آیا از لغو سفارش شمارهی #${this.heldOrder.orderNumber} اطمینان دارید؟`,
|
||||||
|
header: 'لغو سفارش موقت',
|
||||||
|
acceptLabel: 'بله',
|
||||||
|
rejectLabel: 'خیر',
|
||||||
|
accept: () => {
|
||||||
|
this.actionLoading.set(true);
|
||||||
|
// this.posService.cancelOrder(this.posId, heldOrderId).subscribe({
|
||||||
|
// next: () => {
|
||||||
|
// this.onCancel.emit(heldOrderId);
|
||||||
|
// this.actionLoading.set(false);
|
||||||
|
// this.toastService.success({
|
||||||
|
// text: `سفارش شمارهی #${this.heldOrder.orderNumber} با موفقیت لغو شد.`,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// error: () => {
|
||||||
|
// this.actionLoading.set(false);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHeldOrder(heldOrderId: number) {
|
||||||
|
this.onLoad.emit(heldOrderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="d-flex flex-col gap-4">
|
||||||
|
<div class="flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-2 text-muted-color">
|
||||||
|
<i class="pi pi-shopping-cart"></i>
|
||||||
|
<span class="text-base font-bold">سفارشهای نگهداشته شده</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!heldOrders()?.length) {
|
||||||
|
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
|
||||||
|
<span class="text-base text-muted-color pt-4">هیچ سفارش نگهداشته شدهای وجود ندارد.</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="flex flex-col gap-2 mt-3">
|
||||||
|
@for (heldOrder of heldOrders(); track heldOrder.id) {
|
||||||
|
<app-held-order-item [heldOrder]="heldOrder" (onCancel)="removeHeldOrder($event)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { IPosHeldOrderResponse } from '@/modules/pos/models';
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { PosService } from '../../services/main.service';
|
||||||
|
import { HeldOrderItemComponent } from './held-order-item.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-held-orders',
|
||||||
|
templateUrl: './held-orders.component.html',
|
||||||
|
imports: [HeldOrderItemComponent],
|
||||||
|
})
|
||||||
|
export class HeldOrdersComponent {
|
||||||
|
private readonly service = inject(PosService);
|
||||||
|
|
||||||
|
heldOrders = signal<Maybe<IPosHeldOrderResponse[]>>(null);
|
||||||
|
loading = signal<boolean>(false);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getHeldOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeldOrders() {
|
||||||
|
this.loading.set(true);
|
||||||
|
// this.service.getHeldOrders().subscribe({
|
||||||
|
// next: (res) => {
|
||||||
|
// this.heldOrders.set(res.data);
|
||||||
|
// this.loading.set(false);
|
||||||
|
// },
|
||||||
|
// error: () => {
|
||||||
|
// this.loading.set(false);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHeldOrder(orderId: number) {
|
||||||
|
const currentOrders = this.heldOrders() || [];
|
||||||
|
this.heldOrders.set(currentOrders.filter((order) => order.id !== orderId));
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
<div class="border border-surface-border rounded-xl p-2 shadow-sm">
|
||||||
|
<div class="flex gap-3 items-stretch">
|
||||||
|
<img [src]="placeholderImage" class="w-24 h-24 rounded-md bg-surface-100 object-cover shrink-0" />
|
||||||
|
<div class="flex flex-col grow justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<span class="font-bold text-lg">{{ inOrderGood.good.name }}</span>
|
||||||
|
<span class="text-sm text-muted-color">{{ inOrderGood.good.sku }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex shrink-0 gap-1">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
class="ms-auto"
|
||||||
|
(click)="editOrderItem()"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
class="ms-auto"
|
||||||
|
(click)="removeGoodFromOrder()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 mt-auto">
|
||||||
|
<div class="ms-auto shrink-0">
|
||||||
|
<!-- <app-uikit-counter
|
||||||
|
[min]="1"
|
||||||
|
[max]="inOrderGood.maxQuantity"
|
||||||
|
[value]="inOrderGood.count"
|
||||||
|
(valueChange)="updateInOrderGoodQuantity(inOrderGood.goodId, $event)"
|
||||||
|
></app-uikit-counter> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pos-payload-form-dialog
|
||||||
|
[(visible)]="visible"
|
||||||
|
[good]="inOrderGood.good"
|
||||||
|
[initialValues]="inOrderGood"
|
||||||
|
[orderItemId]="inOrderGood.id"
|
||||||
|
/>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component, inject, Input, signal } from '@angular/core';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import images from 'src/assets/images';
|
||||||
|
import { IPosInOrderGood } from '../../models';
|
||||||
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
import { PayloadFormDialogComponent } from '../payload-form.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-order-card-template',
|
||||||
|
templateUrl: './order-card-template.component.html',
|
||||||
|
imports: [ButtonDirective, PayloadFormDialogComponent],
|
||||||
|
})
|
||||||
|
export class OrderCardTemplateComponent {
|
||||||
|
@Input({ required: true }) inOrderGood!: IPosInOrderGood;
|
||||||
|
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
visible = signal(false);
|
||||||
|
|
||||||
|
placeholderImage = images.placeholders.default;
|
||||||
|
|
||||||
|
removeGoodFromOrder() {
|
||||||
|
this.store.removeFromInOrderGoods(this.inOrderGood.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
editOrderItem() {
|
||||||
|
this.visible.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
|
||||||
|
<div class="grow overflow-auto flex flex-col">
|
||||||
|
<div class="flex gap-2 items-center justify-between shrink-0">
|
||||||
|
<app-key-value label="مشتری" [value]="customerNameToShow()" />
|
||||||
|
<button pButton outlined icon="pi pi-pencil" size="small" (click)="openCustomerDialog()"></button>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-2 text-muted-color">
|
||||||
|
<i class="pi pi-shopping-cart"></i>
|
||||||
|
<span class="text-base font-bold">سفارشها</span>
|
||||||
|
</div>
|
||||||
|
<button pButton type="button" icon="pi pi-refresh" outlined size="small" (click)="clearOrderList()">
|
||||||
|
پاک کردن سفارشها
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (!inOrderGoods().length) {
|
||||||
|
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
|
||||||
|
<i class="pi pi-inbox text-6xl!"></i>
|
||||||
|
<span class="text-lg text-muted-color">هیچ سفارشی ثبت نشده است.</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="flex flex-col gap-2 mt-3">
|
||||||
|
@for (inOrderGood of inOrderGoods(); track inOrderGood.id) {
|
||||||
|
@if (inOrderGood.good.pricing_model === "GOLD") {
|
||||||
|
<gold-payload-order-card [orderItem]="inOrderGood" />
|
||||||
|
} @else {
|
||||||
|
<standard-payload-order-card [orderItem]="inOrderGood" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col sticky bottom-0 py-2">
|
||||||
|
<pos-order-price-info-card />
|
||||||
|
<div class="sticky bottom-0 pt-4">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="ثبت و پرداخت"
|
||||||
|
severity="primary"
|
||||||
|
icon="pi pi-check"
|
||||||
|
class="w-full"
|
||||||
|
[attr.disabled]="inOrderGoods().length === 0 ? true : null"
|
||||||
|
(click)="submitAndPay()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
|
||||||
|
<!-- <pos-pre-invoice-dialog [(visible)]="isVisiblePreInvoiceForm" /> -->
|
||||||
|
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment()" />
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
|
||||||
|
// import { ICustomerResponse } from '@/modules/customers/models';
|
||||||
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import images from 'src/assets/images';
|
||||||
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
import { PosOrderCustomerDialogComponent } from '../customers/customer-dialog.component';
|
||||||
|
import { GoldPayloadOrderCardComponent } from '../payloads/gold/order-card.component';
|
||||||
|
import { StandardPayloadOrderCardComponent } from '../payloads/standard/order-card.component';
|
||||||
|
import { PosPaymentFormDialogComponent } from '../payment/form-dialog.component';
|
||||||
|
import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-order-section',
|
||||||
|
templateUrl: './order-section.component.html',
|
||||||
|
imports: [
|
||||||
|
ButtonDirective,
|
||||||
|
POSOrderPriceInfoCardComponent,
|
||||||
|
FormsModule,
|
||||||
|
GoldPayloadOrderCardComponent,
|
||||||
|
StandardPayloadOrderCardComponent,
|
||||||
|
PosOrderCustomerDialogComponent,
|
||||||
|
KeyValueComponent,
|
||||||
|
PosPaymentFormDialogComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PosOrderSectionComponent {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
placeholderImage = images.placeholders.default;
|
||||||
|
|
||||||
|
isVisibleCustomerForm = signal(false);
|
||||||
|
isVisiblePaymentForm = signal(false);
|
||||||
|
isVisiblePreInvoiceForm = signal(false);
|
||||||
|
|
||||||
|
inOrderGoods = computed(() => this.store.inOrderGoods());
|
||||||
|
customer = computed(() => this.store.customer());
|
||||||
|
customerNameToShow = computed(() => {
|
||||||
|
const type = this.store.customer().type;
|
||||||
|
const info = this.store.customer().info;
|
||||||
|
let customerNameToShow = 'نامشخص';
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'UNKNOWN':
|
||||||
|
customerNameToShow = `${info?.customerUnknown?.name || 'نامشخص'} (نوع دوم)`;
|
||||||
|
break;
|
||||||
|
case 'INDIVIDUAL':
|
||||||
|
customerNameToShow = `${info?.customerIndividuals ? info?.customerIndividuals.first_name + ' ' + info?.customerIndividuals.last_name : 'نامشخص'} (نوع اول)`;
|
||||||
|
break;
|
||||||
|
case 'LEGAL':
|
||||||
|
customerNameToShow = `${info?.customerLegals?.company_name || 'نامشخص'} (نوع اول)`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return customerNameToShow;
|
||||||
|
});
|
||||||
|
|
||||||
|
clearOrderList() {
|
||||||
|
this.store.resetInOrderGoods();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.store.submitOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
openCustomerDialog() {
|
||||||
|
// this.this.store.submitOrder();
|
||||||
|
this.isVisibleCustomerForm.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitAndPay() {
|
||||||
|
// this.this.store.submitOrder();
|
||||||
|
this.isVisiblePaymentForm.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitCustomer() {
|
||||||
|
console.log('submitCustomer');
|
||||||
|
}
|
||||||
|
submitPayment() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<p-card class="border border-surface-border">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>جمع قیمت</span>
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<!-- @if (priceInfo().discountAmount) {
|
||||||
|
<span class="line-through text-muted-color text-xs" [appPriceMask]="priceInfo().totalBaseAmount"></span>
|
||||||
|
}
|
||||||
|
<span [appPriceMask]="priceInfo().totalBaseAmount - priceInfo().discountAmount"></span>-->
|
||||||
|
<span [appPriceMask]="priceInfo().totalBaseAmount"></span>
|
||||||
|
<!-- </div>
|
||||||
|
} @else {
|
||||||
|
<span [appPriceMask]="priceInfo().totalBaseAmount"></span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>تخفیف</span>
|
||||||
|
<span [appPriceMask]="priceInfo().discountAmount"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>مالیات</span>
|
||||||
|
<span [appPriceMask]="priceInfo().taxAmount"></span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="flex justify-between items-center font-bold text-lg">
|
||||||
|
<span>مجموع کل</span>
|
||||||
|
<span [appPriceMask]="priceInfo().totalAmount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, computed, inject } from '@angular/core';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-order-price-info-card',
|
||||||
|
templateUrl: './price-info-card.component.html',
|
||||||
|
imports: [Card, PriceMaskDirective],
|
||||||
|
})
|
||||||
|
export class POSOrderPriceInfoCardComponent {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
priceInfo = computed(() => this.store.orderPricingInfo());
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<p-dialog
|
||||||
|
[(visible)]="visible"
|
||||||
|
[header]="preparedTitle()"
|
||||||
|
[modal]="true"
|
||||||
|
[style]="{ width: '500px' }"
|
||||||
|
[closable]="true"
|
||||||
|
(onHide)="close()"
|
||||||
|
>
|
||||||
|
@if (good) {
|
||||||
|
@if (isGoldMode()) {
|
||||||
|
<pos-gold-payload-form [initialValues]="goldPayload()" (onSubmit)="submit($event)" />
|
||||||
|
} @else if (isStandardMode()) {
|
||||||
|
<pos-standard-payload-form
|
||||||
|
[initialValues]="standardPayload()"
|
||||||
|
[unitType]="good.unit_type"
|
||||||
|
(onSubmit)="submit($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||||
|
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||||
|
import { Dialog } from 'primeng/dialog';
|
||||||
|
import { IGoldPayload, IPosOrderItem, IStandardPayload, TPosOrderGoodPayload } from '../models';
|
||||||
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
import { PosGoldPayloadFormComponent } from './payloads/gold/form.component';
|
||||||
|
import { PosStandardPayloadFormComponent } from './payloads/standard/form.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-payload-form-dialog',
|
||||||
|
templateUrl: './payload-form.component.html',
|
||||||
|
imports: [PosGoldPayloadFormComponent, Dialog, PosStandardPayloadFormComponent],
|
||||||
|
})
|
||||||
|
export class PayloadFormDialogComponent extends AbstractDialog {
|
||||||
|
@Input({ required: true }) good!: IGoodResponse;
|
||||||
|
@Input() initialValues?: IPosOrderItem;
|
||||||
|
@Input() orderItemId?: string;
|
||||||
|
@Output() onSubmit = new EventEmitter<TPosOrderGoodPayload>();
|
||||||
|
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
|
||||||
|
totalAmount = signal<number>(0);
|
||||||
|
preparedTitle = computed(() => this.good?.name);
|
||||||
|
|
||||||
|
editMode = computed(() => Boolean(this.initialValues) && Boolean(this.orderItemId));
|
||||||
|
isGoldMode = computed(() => this.good.pricing_model === 'GOLD');
|
||||||
|
isStandardMode = computed(() => this.good.pricing_model === 'STANDARD');
|
||||||
|
|
||||||
|
goldPayload = computed(() =>
|
||||||
|
this.isGoldMode() && this.editMode()
|
||||||
|
? (this.initialValues as IPosOrderItem<IGoldPayload>)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
standardPayload = computed(() =>
|
||||||
|
this.isStandardMode() && this.editMode()
|
||||||
|
? (this.initialValues as IPosOrderItem<IStandardPayload>)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
onChangeTotalAmount = (totalAmount: number) => {
|
||||||
|
this.totalAmount.set(totalAmount);
|
||||||
|
};
|
||||||
|
|
||||||
|
submit(payload: IPosOrderItem) {
|
||||||
|
if (this.editMode()) {
|
||||||
|
this.store.editInOrderGoods({ id: this.orderItemId!, good: this.good, ...payload });
|
||||||
|
} else {
|
||||||
|
this.store.addToInOrderGoods(this.good.id, payload);
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
<p-card class="border-none w-full">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>جمع قیمت</span>
|
||||||
|
@if (!discountAmount) {
|
||||||
|
<span [appPriceMask]="baseTotalAmount"></span>
|
||||||
|
} @else {
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<span class="line-through text-muted-color text-xs" [appPriceMask]="baseTotalAmount"></span>
|
||||||
|
<span [appPriceMask]="baseTotalAmount - discountAmount"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>مالیات (۱۰٪)</span>
|
||||||
|
<span [appPriceMask]="taxAmount"></span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="flex justify-between items-center font-bold text-lg">
|
||||||
|
<span>مجموع کل</span>
|
||||||
|
<span [appPriceMask]="totalAmount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-form-dialog-amount-card-template',
|
||||||
|
templateUrl: './form-dialog-amount-card-template.component.html',
|
||||||
|
imports: [Card, PriceMaskDirective],
|
||||||
|
})
|
||||||
|
export class PosFormDialogAmountCardTemplateComponent {
|
||||||
|
@Input({ required: true }) baseTotalAmount!: number;
|
||||||
|
@Input({ required: true }) totalAmount!: number;
|
||||||
|
@Input({ required: true }) discountAmount!: number;
|
||||||
|
@Input({ required: true }) taxAmount!: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<form [formGroup]="form">
|
||||||
|
<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-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" />
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.payload.controls.wages"
|
||||||
|
name="wages"
|
||||||
|
[label]="`اجرت ${toPriceFormat(wagePrice())}`"
|
||||||
|
type="number"
|
||||||
|
suffix="درصد"
|
||||||
|
[min]="0"
|
||||||
|
[max]="100"
|
||||||
|
/>
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.payload.controls.commission"
|
||||||
|
name="commission"
|
||||||
|
[label]="`حقالعمل ${toPriceFormat(commissionPrice())}`"
|
||||||
|
type="number"
|
||||||
|
suffix="درصد"
|
||||||
|
[min]="0"
|
||||||
|
[max]="100"
|
||||||
|
/>
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.payload.controls.profit"
|
||||||
|
name="profit"
|
||||||
|
[label]="`سود ${toPriceFormat(profitPrice())}`"
|
||||||
|
type="number"
|
||||||
|
suffix="درصد"
|
||||||
|
[min]="0"
|
||||||
|
[max]="100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.discount"
|
||||||
|
name="discount"
|
||||||
|
label="تخفیف"
|
||||||
|
type="number"
|
||||||
|
suffix="درصد"
|
||||||
|
[min]="0"
|
||||||
|
[max]="100"
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<div class="flex flex-col gap-4 justify-center w-full">
|
||||||
|
<pos-form-dialog-amount-card-template
|
||||||
|
[totalAmount]="totalAmount()"
|
||||||
|
[baseTotalAmount]="baseTotalAmount()"
|
||||||
|
[discountAmount]="discountAmount()"
|
||||||
|
[taxAmount]="taxAmount()"
|
||||||
|
/>
|
||||||
|
<p-button (onClick)="submit()"> افزودن به لیست خرید </p-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
|
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
|
||||||
|
import { formatWithCurrency } from '@/utils';
|
||||||
|
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { Button } from 'primeng/button';
|
||||||
|
import { IGoldPayload, IPosOrderItem } from '../../../models';
|
||||||
|
import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-card-template.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-gold-payload-form',
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputComponent,
|
||||||
|
EnumSelectComponent,
|
||||||
|
Button,
|
||||||
|
PosFormDialogAmountCardTemplateComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PosGoldPayloadFormComponent extends AbstractForm<
|
||||||
|
IPosOrderItem<IGoldPayload>,
|
||||||
|
IPosOrderItem<IGoldPayload>
|
||||||
|
> {
|
||||||
|
@Output() onSubmitForm = new EventEmitter<IGoldPayload>();
|
||||||
|
@Output() onChangeTotalAmount = new EventEmitter<number>();
|
||||||
|
|
||||||
|
private readonly initialForm = () => {
|
||||||
|
const form = this.fb.group({
|
||||||
|
unit_price: [this.initialValues?.unit_price || 200_000_000, [Validators.required]],
|
||||||
|
quantity: [this.initialValues?.quantity || 2, [Validators.required]],
|
||||||
|
discount: [this.initialValues?.discount || 0],
|
||||||
|
payload: this.fb.group({
|
||||||
|
commission: [this.initialValues?.payload?.commission || 10, [Validators.required]],
|
||||||
|
karat: [this.initialValues?.payload?.karat || '', [Validators.required]],
|
||||||
|
wages: [this.initialValues?.payload?.wages || 10, [Validators.required]],
|
||||||
|
profit: [this.initialValues?.payload?.profit || 10, [Validators.required]],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
form.valueChanges.subscribe((value) => {
|
||||||
|
this.updateCalculateAmount(value as IPosOrderItem<IGoldPayload>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return form;
|
||||||
|
};
|
||||||
|
|
||||||
|
form = this.initialForm();
|
||||||
|
override submitForm(payload: IPosOrderItem) {
|
||||||
|
this.onSubmit.emit({
|
||||||
|
...payload,
|
||||||
|
total_amount: this.totalAmount(),
|
||||||
|
base_total_amount: this.baseTotalAmount(),
|
||||||
|
discount_amount: this.discountAmount(),
|
||||||
|
tax_amount: this.taxAmount(),
|
||||||
|
payload: {
|
||||||
|
commission_amount: this.commissionPrice(),
|
||||||
|
profit_amount: this.profitPrice(),
|
||||||
|
wages_amount: this.wagePrice(),
|
||||||
|
commission: this.form.controls.payload.controls.commission.value || 0,
|
||||||
|
karat: this.form.controls.payload.controls.karat.value as TGoldKarat,
|
||||||
|
profit: this.form.controls.payload.controls.profit.value || 0,
|
||||||
|
wages: this.form.controls.payload.controls.wages.value || 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wagePrice = signal<number>(0);
|
||||||
|
karatPrice = signal<number>(0);
|
||||||
|
commissionPrice = signal<number>(0);
|
||||||
|
profitPrice = signal<number>(0);
|
||||||
|
totalAmount = signal<number>(0);
|
||||||
|
baseTotalAmount = signal<number>(0);
|
||||||
|
discountAmount = signal<number>(0);
|
||||||
|
taxAmount = signal<number>(0);
|
||||||
|
|
||||||
|
toPriceFormat(amount: number) {
|
||||||
|
if (!amount) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `(${formatWithCurrency(amount)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCalculateAmount(value: Partial<IPosOrderItem<IGoldPayload>>) {
|
||||||
|
const unitWithQuantity = (value.unit_price ?? 0) * (value.quantity ?? 0);
|
||||||
|
const commissionPrice = (unitWithQuantity * Number(value.payload?.commission ?? '0')) / 100;
|
||||||
|
const wagePrice = (unitWithQuantity * Number(value.payload?.wages ?? '0')) / 100;
|
||||||
|
const totalPriceBeforeProfit = unitWithQuantity + wagePrice + commissionPrice;
|
||||||
|
|
||||||
|
const profitPrice = (totalPriceBeforeProfit * Number(value.payload?.profit ?? '0')) / 100;
|
||||||
|
|
||||||
|
const baseTotalAmount = unitWithQuantity + profitPrice + commissionPrice + wagePrice;
|
||||||
|
|
||||||
|
const discountPrice = (baseTotalAmount * Number(value.discount || '0')) / 100;
|
||||||
|
const taxAmount = (profitPrice + commissionPrice + wagePrice - discountPrice) * 0.1;
|
||||||
|
|
||||||
|
this.wagePrice.set(wagePrice);
|
||||||
|
this.commissionPrice.set(commissionPrice);
|
||||||
|
this.profitPrice.set(profitPrice);
|
||||||
|
this.discountAmount.set(discountPrice);
|
||||||
|
this.taxAmount.set(taxAmount);
|
||||||
|
this.baseTotalAmount.set(baseTotalAmount);
|
||||||
|
|
||||||
|
this.totalAmount.set(baseTotalAmount - discountPrice + taxAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<pos-order-card-template [inOrderGood]="orderItem"></pos-order-card-template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { IPosInOrderGood } from '../../../models';
|
||||||
|
import { OrderCardTemplateComponent } from '../../order/order-card-template.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gold-payload-order-card',
|
||||||
|
templateUrl: './order-card.component.html',
|
||||||
|
imports: [OrderCardTemplateComponent],
|
||||||
|
})
|
||||||
|
export class GoldPayloadOrderCardComponent {
|
||||||
|
@Input({ required: true }) orderItem!: IPosInOrderGood;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from '../payload-form.component';
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<app-input [control]="form.controls.unit_price" name="unit_price" label="قیمت پایه" type="price" />
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.quantity"
|
||||||
|
name="quantity"
|
||||||
|
[label]="preparedFormVisuals().quantityLabel"
|
||||||
|
type="number"
|
||||||
|
[suffix]="preparedFormVisuals().quantitySymbolText"
|
||||||
|
[min]="0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.discount"
|
||||||
|
name="discount"
|
||||||
|
label="تخفیف"
|
||||||
|
type="number"
|
||||||
|
suffix="درصد"
|
||||||
|
[min]="0"
|
||||||
|
[max]="100"
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<div class="flex flex-col gap-4 justify-center w-full">
|
||||||
|
<pos-form-dialog-amount-card-template
|
||||||
|
[totalAmount]="totalAmount()"
|
||||||
|
[baseTotalAmount]="baseTotalAmount()"
|
||||||
|
[discountAmount]="discountAmount()"
|
||||||
|
[taxAmount]="taxAmount()"
|
||||||
|
/>
|
||||||
|
<p-button (onClick)="submit()"> افزودن به لیست خرید </p-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { formatWithCurrency, getGoodUnitTypeProperties, UnitType } from '@/utils';
|
||||||
|
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { Button } from 'primeng/button';
|
||||||
|
import { IPosOrderItem, IStandardPayload } from '../../../models';
|
||||||
|
import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-card-template.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-standard-payload-form',
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
imports: [ReactiveFormsModule, InputComponent, Button, PosFormDialogAmountCardTemplateComponent],
|
||||||
|
})
|
||||||
|
export class PosStandardPayloadFormComponent extends AbstractForm<
|
||||||
|
IPosOrderItem<IStandardPayload>,
|
||||||
|
IPosOrderItem<IStandardPayload>
|
||||||
|
> {
|
||||||
|
@Input({ required: true }) unitType!: UnitType;
|
||||||
|
@Output() onChangeTotalAmount = new EventEmitter<number>();
|
||||||
|
|
||||||
|
private readonly initialForm = () => {
|
||||||
|
const form = this.fb.group({
|
||||||
|
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
|
||||||
|
quantity: [this.initialValues?.quantity || 0, [Validators.required]],
|
||||||
|
discount: [this.initialValues?.discount || ''],
|
||||||
|
});
|
||||||
|
|
||||||
|
form.valueChanges.subscribe((value) => {
|
||||||
|
this.updateCalculateAmount(value as Partial<IPosOrderItem<IStandardPayload>>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return form;
|
||||||
|
};
|
||||||
|
|
||||||
|
form = this.initialForm();
|
||||||
|
override submitForm(payload: IPosOrderItem<IStandardPayload>) {
|
||||||
|
this.onSubmit.emit({
|
||||||
|
...payload,
|
||||||
|
total_amount: this.totalAmount(),
|
||||||
|
discount_amount: this.discountAmount(),
|
||||||
|
tax_amount: this.taxAmount(),
|
||||||
|
base_total_amount: this.baseTotalAmount(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedFormVisuals = computed(() => getGoodUnitTypeProperties(this.unitType));
|
||||||
|
|
||||||
|
baseTotalAmount = signal<number>(0);
|
||||||
|
totalAmount = signal<number>(0);
|
||||||
|
discountAmount = signal<number>(0);
|
||||||
|
taxAmount = signal<number>(0);
|
||||||
|
|
||||||
|
toPriceFormat(amount: number) {
|
||||||
|
if (!amount) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `(${formatWithCurrency(amount)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCalculateAmount(value: Partial<IPosOrderItem<IStandardPayload>>) {
|
||||||
|
const baseTotalAmount = (value.unit_price ?? 0) * (value.quantity ?? 0);
|
||||||
|
const discountAmount = (baseTotalAmount * Number(value.discount || '0')) / 100;
|
||||||
|
const baseTotalAmountWithoutTax = baseTotalAmount - discountAmount;
|
||||||
|
const taxAmount = baseTotalAmountWithoutTax * 0.1;
|
||||||
|
|
||||||
|
this.baseTotalAmount.set(baseTotalAmount);
|
||||||
|
this.discountAmount.set(discountAmount);
|
||||||
|
this.taxAmount.set(taxAmount);
|
||||||
|
this.totalAmount.set(baseTotalAmountWithoutTax + taxAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<pos-order-card-template [inOrderGood]="orderItem"></pos-order-card-template>
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { IPosInOrderGood } from '../../../models';
|
||||||
|
import { OrderCardTemplateComponent } from '../../order/order-card-template.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'standard-payload-order-card',
|
||||||
|
templateUrl: './order-card.component.html',
|
||||||
|
imports: [OrderCardTemplateComponent],
|
||||||
|
})
|
||||||
|
export class StandardPayloadOrderCardComponent {
|
||||||
|
@Input({ required: true }) orderItem!: IPosInOrderGood;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<p-dialog
|
||||||
|
[(visible)]="visible"
|
||||||
|
header="اطلاعات پرداخت"
|
||||||
|
[modal]="true"
|
||||||
|
[style]="{ width: '500px' }"
|
||||||
|
[closable]="true"
|
||||||
|
(onHide)="close()"
|
||||||
|
>
|
||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
|
||||||
|
<app-key-value label="مبلغ باقیمانده" [value]="remainedAmount().toString()" type="price" />
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<app-input
|
||||||
|
[control]="form.controls.terminal"
|
||||||
|
name="terminal"
|
||||||
|
label="پرداخت با پایانه"
|
||||||
|
type="price"
|
||||||
|
[max]="terminalMax()"
|
||||||
|
>
|
||||||
|
<ng-template #suffixTemp>
|
||||||
|
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('TERMINAL')">
|
||||||
|
تمامی بدهی باقیمانده
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
</app-input>
|
||||||
|
<app-input [control]="form.controls.cash" name="cash" label="نقدی" type="price" [max]="cashMax()">
|
||||||
|
<ng-template #suffixTemp>
|
||||||
|
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('CASH')">
|
||||||
|
تمامی بدهی باقیمانده
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
</app-input>
|
||||||
|
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
|
||||||
|
<ng-template #suffixTemp>
|
||||||
|
<button pButton size="small" outlined type="button" class="border-s-0!" (click)="fillRemained('SET_OFF')">
|
||||||
|
تمامی بدهی باقیمانده
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
</app-input>
|
||||||
|
|
||||||
|
<app-form-footer-actions
|
||||||
|
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
||||||
|
[loading]="submitOrderLoading()"
|
||||||
|
(onCancel)="close()"
|
||||||
|
(onSubmit)="submit()"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||||
|
import { InputComponent, KeyValueComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { Dialog } from 'primeng/dialog';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IPayment, TOrderPaymentTypes } from '../../models';
|
||||||
|
import { PosLandingStore } from '../../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-payment-form-dialog',
|
||||||
|
templateUrl: './form-dialog.component.html',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputComponent,
|
||||||
|
FormFooterActionsComponent,
|
||||||
|
KeyValueComponent,
|
||||||
|
ButtonDirective,
|
||||||
|
Dialog,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPayment> {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
private readonly toastServices = inject(ToastService);
|
||||||
|
|
||||||
|
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
|
||||||
|
|
||||||
|
readonly remainedAmount = signal(this.totalAmount());
|
||||||
|
setOffMax = signal(this.remainedAmount());
|
||||||
|
cashMax = signal(this.remainedAmount());
|
||||||
|
terminalMax = signal(this.remainedAmount());
|
||||||
|
|
||||||
|
submitOrderLoading = computed(() => this.store.submitOrderLoading());
|
||||||
|
|
||||||
|
private initForm = () => {
|
||||||
|
const form = this.fb.group({
|
||||||
|
set_off: [this.initialValues?.set_off || 0],
|
||||||
|
cash: [this.initialValues?.cash || 0],
|
||||||
|
terminal: [this.initialValues?.terminal || 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
form.valueChanges.subscribe((value) => {
|
||||||
|
const totalValue = (value.cash || 0) + (value.set_off || 0) + (value.terminal || 0);
|
||||||
|
const remainedAmount = this.totalAmount() - totalValue;
|
||||||
|
|
||||||
|
const setOffMax = remainedAmount + (value.set_off || 0);
|
||||||
|
const cashMax = remainedAmount + (value.cash || 0);
|
||||||
|
const terminalMax = remainedAmount + (value.terminal || 0);
|
||||||
|
|
||||||
|
form.controls.set_off.clearValidators();
|
||||||
|
form.controls.set_off.addValidators([Validators.max(setOffMax)]);
|
||||||
|
form.controls.cash.clearValidators();
|
||||||
|
form.controls.cash.addValidators([Validators.max(cashMax)]);
|
||||||
|
form.controls.terminal.clearValidators();
|
||||||
|
form.controls.terminal.addValidators([Validators.max(terminalMax)]);
|
||||||
|
|
||||||
|
this.remainedAmount.set(remainedAmount);
|
||||||
|
this.cashMax.set(cashMax);
|
||||||
|
this.terminalMax.set(terminalMax);
|
||||||
|
this.setOffMax.set(setOffMax);
|
||||||
|
});
|
||||||
|
|
||||||
|
return form;
|
||||||
|
};
|
||||||
|
|
||||||
|
form = this.initForm();
|
||||||
|
|
||||||
|
fillRemained(type: TOrderPaymentTypes) {
|
||||||
|
switch (type) {
|
||||||
|
case 'TERMINAL':
|
||||||
|
this.form.controls.terminal.setValue(
|
||||||
|
(this.form.controls.terminal.value || 0) + this.remainedAmount(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CASH':
|
||||||
|
this.form.controls.cash.setValue(
|
||||||
|
(this.form.controls.cash.value || 0) + this.remainedAmount(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SET_OFF':
|
||||||
|
this.form.controls.set_off.setValue(
|
||||||
|
(this.form.controls.set_off.value || 0) + this.remainedAmount(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override submitForm() {
|
||||||
|
if (this.remainedAmount() > 0) {
|
||||||
|
return this.toastServices.warn({
|
||||||
|
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = this.form.value as IPayment;
|
||||||
|
this.store.setPayment(payment);
|
||||||
|
|
||||||
|
this.store.submitOrder().then((res) => {
|
||||||
|
if (res) {
|
||||||
|
this.close();
|
||||||
|
this.toastServices.success({
|
||||||
|
text: 'فاکتور شما با موفقیت ایجاد شد',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Observable<IPayment>((observer) => observer.next(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
override onSuccess(response: IPayment): void {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<p-dialog
|
||||||
|
[(visible)]="visible"
|
||||||
|
header="پیش فاکتور"
|
||||||
|
[modal]="true"
|
||||||
|
[style]="{ width: '500px' }"
|
||||||
|
[closable]="true"
|
||||||
|
(onHide)="close()"
|
||||||
|
>
|
||||||
|
<form [formGroup]="form"></form>
|
||||||
|
<uikit-datepicker [control]="form.controls.invoiceDate" label="تاریخ فاکتور" />
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||||
|
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { Dialog } from 'primeng/dialog';
|
||||||
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pos-pre-invoice-dialog',
|
||||||
|
templateUrl: './pre-invoice-dialog.component.html',
|
||||||
|
imports: [Dialog, UikitFlatpickrJalaliComponent, ReactiveFormsModule],
|
||||||
|
})
|
||||||
|
export class PosPreInvoiceDialogComponent extends AbstractDialog {
|
||||||
|
private readonly store = inject(PosLandingStore);
|
||||||
|
private readonly fb = inject(FormBuilder);
|
||||||
|
|
||||||
|
private initForm() {
|
||||||
|
const form = this.fb.group({
|
||||||
|
invoiceDate: [this.store.invoiceDate(), [Validators.required]],
|
||||||
|
});
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
form = this.initForm();
|
||||||
|
}
|
||||||
@@ -3,4 +3,6 @@ const baseUrl = '/api/v1/pos';
|
|||||||
export const POS_API_ROUTES = {
|
export const POS_API_ROUTES = {
|
||||||
info: () => `${baseUrl}`,
|
info: () => `${baseUrl}`,
|
||||||
goods: () => `${baseUrl}/goods`,
|
goods: () => `${baseUrl}/goods`,
|
||||||
|
goodCategories: () => `${baseUrl}/good_categories`,
|
||||||
|
submitOrder: () => `${baseUrl}/sales_invoices`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { TAccountType } from '@/core/constants/accountTypes.const';
|
|
||||||
|
|
||||||
export interface IAccountRawResponse {
|
|
||||||
username: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
export interface IAccountResponse extends IAccountRawResponse {}
|
|
||||||
|
|
||||||
export interface IAccountRequest {
|
|
||||||
username: string;
|
|
||||||
password?: string;
|
|
||||||
type: TAccountType;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export interface IIndividualCustomer {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
national_id: string;
|
||||||
|
postal_code: string;
|
||||||
|
economic_code: string;
|
||||||
|
is_favorite?: boolean;
|
||||||
|
}
|
||||||
|
export interface ILegalCustomer {
|
||||||
|
company_name: string;
|
||||||
|
economic_code: string;
|
||||||
|
registration_number: string;
|
||||||
|
postal_code: string;
|
||||||
|
is_favorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUnknownCustomer {
|
||||||
|
national_id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICustomer {
|
||||||
|
customerIndividuals?: IIndividualCustomer;
|
||||||
|
customerLegals?: ILegalCustomer;
|
||||||
|
customerUnknown?: IUnknownCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCustomerInfo = IIndividualCustomer | ILegalCustomer | IUnknownCustomer;
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
export * from './accounts_io';
|
export * from './customer';
|
||||||
export * from './io';
|
export * from './io';
|
||||||
|
export * from './payload';
|
||||||
|
export * from './payment';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
+15
-15
@@ -1,18 +1,18 @@
|
|||||||
import { TRoles } from '@/core';
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { ICustomer } from './customer';
|
||||||
|
import { IPayment } from './payment';
|
||||||
|
import { IPosOrderItem } from './types';
|
||||||
|
|
||||||
export interface IUserRawResponse {
|
export interface IPosOrderRequest {
|
||||||
mobile_number: string;
|
total_amount: number;
|
||||||
first_name: string;
|
invoice_date: string;
|
||||||
last_name: string;
|
payments: IPayment;
|
||||||
fullname: string;
|
items: IPosOrderItem[];
|
||||||
id: string;
|
notes?: string;
|
||||||
role: TRoles;
|
customer_type?: CustomerType;
|
||||||
|
customerId?: string;
|
||||||
|
customer?: ICustomer;
|
||||||
}
|
}
|
||||||
export interface IUserResponse extends IUserRawResponse {}
|
|
||||||
|
|
||||||
export interface IUserRequest {
|
export interface IPosOrderRawResponse {}
|
||||||
first_name: string;
|
export interface IPosOrderResponse extends IPosOrderRawResponse {}
|
||||||
last_name: string;
|
|
||||||
mobile_number: string;
|
|
||||||
role: TRoles;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
|
||||||
|
|
||||||
|
export interface IGoldPayload {
|
||||||
|
karat: TGoldKarat;
|
||||||
|
wages: number;
|
||||||
|
profit: number;
|
||||||
|
commission: number;
|
||||||
|
wages_amount: number;
|
||||||
|
profit_amount: number;
|
||||||
|
commission_amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStandardPayload {}
|
||||||
|
|
||||||
|
export type TPosOrderGoodPayload = IGoldPayload | IStandardPayload;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface IPayment {
|
||||||
|
terminal: number;
|
||||||
|
cash: number;
|
||||||
|
set_off: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TOrderPaymentTypes = 'TERMINAL' | 'CASH' | 'SET_OFF';
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { UnitType } from '@/utils';
|
||||||
|
import { TPosOrderGoodPayload } from './payload';
|
||||||
|
|
||||||
|
export interface IPosOrderItem<T = TPosOrderGoodPayload> {
|
||||||
|
unit_price: number;
|
||||||
|
quantity: number;
|
||||||
|
total_amount: number;
|
||||||
|
discount_amount: number;
|
||||||
|
unit_type: UnitType;
|
||||||
|
good_id?: string;
|
||||||
|
service_id?: string;
|
||||||
|
notes?: string;
|
||||||
|
payload: T;
|
||||||
|
|
||||||
|
base_total_amount: number;
|
||||||
|
discount: number;
|
||||||
|
tax_amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPosInOrderGood extends Omit<IPosOrderItem, 'good_id' | 'service_id' | 'notes'> {
|
||||||
|
id: string;
|
||||||
|
good: IGoodResponse;
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import { IListingResponse } from '@/core/models/service.model';
|
||||||
|
import {
|
||||||
|
IGoodCategoryRawResponse,
|
||||||
|
IGoodCategoryResponse,
|
||||||
|
IGoodRawResponse,
|
||||||
|
IGoodResponse,
|
||||||
|
} from '@/domains/pos/models/good.io';
|
||||||
import { IPosInfoRawResponse, IPosInfoResponse } from '@/domains/pos/models/pos.io';
|
import { IPosInfoRawResponse, IPosInfoResponse } from '@/domains/pos/models/pos.io';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { POS_API_ROUTES } from '../constants';
|
import { POS_API_ROUTES } from '../constants';
|
||||||
import { IUserRawResponse, IUserResponse } from '../models';
|
import { IPosOrderRawResponse, IPosOrderRequest, IPosOrderResponse } from '../models';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PosService {
|
export class PosService {
|
||||||
@@ -14,7 +21,17 @@ export class PosService {
|
|||||||
getInfo(): Observable<IPosInfoResponse> {
|
getInfo(): Observable<IPosInfoResponse> {
|
||||||
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
|
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
|
||||||
}
|
}
|
||||||
getGoods(): Observable<IUserResponse> {
|
getGoods(searchQuery: string): Observable<IListingResponse<IGoodResponse>> {
|
||||||
return this.http.get<IUserRawResponse>(this.apiRoutes.goods());
|
return this.http.get<IListingResponse<IGoodRawResponse>>(this.apiRoutes.goods());
|
||||||
|
}
|
||||||
|
|
||||||
|
getGoodCategories(): Observable<IListingResponse<IGoodCategoryResponse>> {
|
||||||
|
return this.http.get<IListingResponse<IGoodCategoryRawResponse>>(
|
||||||
|
this.apiRoutes.goodCategories(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitOrder(payload: IPosOrderRequest): Observable<IPosOrderResponse> {
|
||||||
|
return this.http.post<IPosOrderRawResponse>(this.apiRoutes.submitOrder(), payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
// import { ICustomerResponse } from '@/modules/customers/models';
|
||||||
|
import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io';
|
||||||
|
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
|
||||||
|
import { createUUID, JALALI_DATE_FORMATS, nowJalali, parseJalali } from '@/utils';
|
||||||
|
import { computed, inject, Injectable, signal } from '@angular/core';
|
||||||
|
import { catchError, finalize, map, Observable, throwError } from 'rxjs';
|
||||||
|
import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models';
|
||||||
|
import { IPosInOrderGood, IPosOrderItem } from '../models/types';
|
||||||
|
import { PosService } from '../services/main.service';
|
||||||
|
|
||||||
|
export type TViewType = 'list' | 'grid';
|
||||||
|
|
||||||
|
interface IPosLandingState {
|
||||||
|
getGoodsLoading: boolean;
|
||||||
|
goods: Maybe<IGoodResponse[]>;
|
||||||
|
getGoodCategoriesLoading: boolean;
|
||||||
|
goodCategories: Maybe<IGoodCategoryResponse[]>;
|
||||||
|
activeGoodCategory: Maybe<string>;
|
||||||
|
inOrderGoods: IPosInOrderGood[];
|
||||||
|
viewType: TViewType;
|
||||||
|
searchQuery: string;
|
||||||
|
customerDetails: {
|
||||||
|
customer_id?: string;
|
||||||
|
info?: ICustomer;
|
||||||
|
type: CustomerType;
|
||||||
|
};
|
||||||
|
invoiceDate: string;
|
||||||
|
orderNote?: string;
|
||||||
|
payments: IPayment;
|
||||||
|
submitOrderLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INITIAL_POS_STATE: IPosLandingState = {
|
||||||
|
getGoodsLoading: true,
|
||||||
|
goods: null,
|
||||||
|
getGoodCategoriesLoading: true,
|
||||||
|
goodCategories: null,
|
||||||
|
activeGoodCategory: null,
|
||||||
|
inOrderGoods: [],
|
||||||
|
viewType: 'grid',
|
||||||
|
searchQuery: '',
|
||||||
|
customerDetails: {
|
||||||
|
type: CustomerType.UNKNOWN,
|
||||||
|
},
|
||||||
|
invoiceDate: nowJalali(JALALI_DATE_FORMATS.NUMERIC),
|
||||||
|
payments: {
|
||||||
|
cash: 0,
|
||||||
|
set_off: 0,
|
||||||
|
terminal: 0,
|
||||||
|
},
|
||||||
|
submitOrderLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'any' })
|
||||||
|
export class PosLandingStore {
|
||||||
|
private readonly service = inject(PosService);
|
||||||
|
|
||||||
|
private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE });
|
||||||
|
|
||||||
|
readonly goods = computed(() => this.state$().goods);
|
||||||
|
readonly getGoodsLoading = computed(() => this.state$().getGoodsLoading);
|
||||||
|
readonly inOrderGoods = computed(() => this.state$().inOrderGoods);
|
||||||
|
readonly goodCategories = computed(() => this.state$().goodCategories);
|
||||||
|
readonly getGoodCategoriesLoading = computed(() => this.state$().getGoodCategoriesLoading);
|
||||||
|
readonly activeGoodCategory = computed(() => this.state$().activeGoodCategory);
|
||||||
|
readonly viewType = computed(() => this.state$().viewType);
|
||||||
|
readonly searchQuery = computed(() => this.state$().searchQuery);
|
||||||
|
readonly submitOrderLoading = computed(() => this.state$().submitOrderLoading);
|
||||||
|
readonly activatedCategoryGoods = computed(() => {
|
||||||
|
if (!this.activeGoodCategory()) {
|
||||||
|
return this.state$().goods;
|
||||||
|
}
|
||||||
|
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly customer = computed(() => this.state$().customerDetails);
|
||||||
|
readonly invoiceDate = computed(() => this.state$().invoiceDate);
|
||||||
|
readonly payments = computed(() => this.state$().payments);
|
||||||
|
|
||||||
|
readonly orderPricingInfo = computed(() => {
|
||||||
|
const totalAmount = 0;
|
||||||
|
const taxAmount = totalAmount * 0.1;
|
||||||
|
return {
|
||||||
|
totalItems: this.inOrderGoods().length,
|
||||||
|
totalBaseAmount: this.inOrderGoods().reduce((acc, curr) => acc + curr.base_total_amount, 0),
|
||||||
|
totalAmount: this.inOrderGoods().reduce((acc, curr) => acc + curr.total_amount, 0),
|
||||||
|
taxAmount: this.inOrderGoods().reduce((acc, curr) => acc + curr.tax_amount, 0),
|
||||||
|
discountAmount: this.inOrderGoods().reduce((acc, curr) => acc + curr.discount_amount, 0),
|
||||||
|
payableAmount: totalAmount + taxAmount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(partial: Partial<IPosLandingState>) {
|
||||||
|
this.state$.set({ ...this.state$(), ...partial });
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.state$.set({ ...INITIAL_POS_STATE });
|
||||||
|
}
|
||||||
|
|
||||||
|
initial() {
|
||||||
|
this.getGoods();
|
||||||
|
this.getGoodCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
fillInitial(data: Partial<IPosLandingState>) {
|
||||||
|
this.state$.set({ ...INITIAL_POS_STATE, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
getGoods() {
|
||||||
|
this.setState({ getGoodsLoading: true });
|
||||||
|
this.service.getGoods(this.state$().searchQuery).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.setState({ goods: res.data, getGoodsLoading: false });
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.setState({ getGoodsLoading: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getGoodCategories() {
|
||||||
|
this.setState({ getGoodCategoriesLoading: true });
|
||||||
|
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({
|
||||||
|
goodCategories: res,
|
||||||
|
getGoodCategoriesLoading: false,
|
||||||
|
activeGoodCategory: res[0]?.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.setState({ getGoodCategoriesLoading: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeActiveCategory(categoryId: string) {
|
||||||
|
this.setState({ activeGoodCategory: categoryId });
|
||||||
|
}
|
||||||
|
|
||||||
|
editInOrderGoods(inOrderGood: IPosInOrderGood) {
|
||||||
|
this.setState({
|
||||||
|
inOrderGoods: [...this.inOrderGoods()].map((orderItem) => {
|
||||||
|
if (inOrderGood.id === orderItem.id) {
|
||||||
|
return inOrderGood;
|
||||||
|
}
|
||||||
|
return orderItem;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addToInOrderGoods(goodId: string, payload: IPosOrderItem) {
|
||||||
|
const good = this.state$().goods?.find((s) => s.id === goodId);
|
||||||
|
|
||||||
|
if (!good) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
inOrderGoods: [
|
||||||
|
...this.inOrderGoods(),
|
||||||
|
{
|
||||||
|
id: createUUID(),
|
||||||
|
good,
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromInOrderGoods(orderItemId: string) {
|
||||||
|
this.setState({
|
||||||
|
inOrderGoods: this.state$().inOrderGoods.filter((p) => p.id !== orderItemId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateViewType(viewType: TViewType) {
|
||||||
|
this.setState({ viewType });
|
||||||
|
}
|
||||||
|
updateSearchQuery(searchQuery: string) {
|
||||||
|
this.setState({ searchQuery });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetInOrderGoods() {
|
||||||
|
this.setState({ inOrderGoods: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomer(customer: { customer_id?: string; info?: ICustomer; type: CustomerType }) {
|
||||||
|
console.log(customer);
|
||||||
|
|
||||||
|
this.setState({ customerDetails: customer });
|
||||||
|
}
|
||||||
|
|
||||||
|
setPayment(payments: IPayment) {
|
||||||
|
this.setState({ payments });
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitOrder() {
|
||||||
|
this.setState({ submitOrderLoading: true });
|
||||||
|
const orderPayload: IPosOrderRequest = {
|
||||||
|
customerId: this.customer().customer_id,
|
||||||
|
customer_type: this.customer().type,
|
||||||
|
customer: this.customer().info,
|
||||||
|
items: this.inOrderGoods().map(({ good, ...rest }) => ({
|
||||||
|
...rest,
|
||||||
|
good_id: good.id,
|
||||||
|
unit_type: good.unit_type,
|
||||||
|
})),
|
||||||
|
invoice_date: parseJalali(this.invoiceDate()).toISOString(),
|
||||||
|
payments: this.payments(),
|
||||||
|
total_amount: this.orderPricingInfo().totalAmount,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: Maybe<IPosOrderResponse> = null;
|
||||||
|
|
||||||
|
await this.service
|
||||||
|
.submitOrder(orderPayload)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.setState({ submitOrderLoading: false });
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
return throwError('');
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe((_res) => {
|
||||||
|
this.reset();
|
||||||
|
res = _res;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
return new Observable<IPosOrderResponse>((observer) => observer.next(res!));
|
||||||
|
}
|
||||||
|
return new Observable((observer) => observer.error());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,8 @@
|
|||||||
<span>صفحهی pos</span>
|
<div class="flex gap-4 grow overflow-hidden h-full">
|
||||||
|
<div class="grow h-full overflow-auto">
|
||||||
|
<pos-goods />
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 h-full">
|
||||||
|
<pos-order-section />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
// import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
|
// import { CatalogRoleTagComponent } from '@/shared/catalog/roles';
|
||||||
import { PosStore } from '@/domains/pos/pos.store';
|
import { Component } from '@angular/core';
|
||||||
import { LayoutService } from '@/layout/service/layout.service';
|
import { PosGoodsComponent } from '../components/goods.component';
|
||||||
import { Component, computed, inject } from '@angular/core';
|
import { PosOrderSectionComponent } from '../components/order/order-section.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-landing',
|
selector: 'pos-landing',
|
||||||
templateUrl: './root.component.html',
|
templateUrl: './root.component.html',
|
||||||
|
host: { class: 'grow overflow-hidden' },
|
||||||
|
|
||||||
|
imports: [PosGoodsComponent, PosOrderSectionComponent],
|
||||||
})
|
})
|
||||||
export class PosLandingComponent {
|
export class PosLandingComponent {}
|
||||||
private readonly store = inject(PosStore);
|
|
||||||
private readonly layoutService = inject(LayoutService);
|
|
||||||
|
|
||||||
private readonly posInfo = computed(() => this.store.entity());
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.layoutService.setPanelInfo({
|
|
||||||
title: `فروشگاه ${this.posInfo()?.complex.name} - ${this.posInfo()?.name}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
|||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class AbstractDialog {
|
export abstract class AbstractDialog {
|
||||||
@Input()
|
@Input({ required: true })
|
||||||
set visible(v: boolean) {
|
set visible(v: boolean) {
|
||||||
this.visibleSignal.set(!!v);
|
this.visibleSignal.set(!!v);
|
||||||
this.visibleChange.emit(!!v);
|
this.visibleChange.emit(!!v);
|
||||||
|
if (!v) {
|
||||||
|
this.onClose.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
get visible() {
|
get visible() {
|
||||||
return this.visibleSignal();
|
return this.visibleSignal();
|
||||||
}
|
}
|
||||||
private visibleSignal = signal(false);
|
private visibleSignal = signal(false);
|
||||||
|
|
||||||
|
@Output() onClose = new EventEmitter<void>();
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TEnumApi } from '../models';
|
import { TEnumApi } from '../models';
|
||||||
|
|
||||||
export const apiEnumTranslates = {
|
export const apiEnumTranslates = {
|
||||||
goldKarat: '',
|
goldKarat: 'عیار',
|
||||||
userStatus: 'وضعیت کاربر',
|
userStatus: 'وضعیت کاربر',
|
||||||
accountRole: 'نقش',
|
accountRole: 'نقش',
|
||||||
accountStatus: 'وضعیت',
|
accountStatus: 'وضعیت',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
[class]="` w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
|
[class]="` w-full ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
|
||||||
[required]="isRequired"
|
[required]="isRequired"
|
||||||
[invalid]="control.invalid && (control.touched || control.dirty)"
|
[invalid]="control.invalid && (control.touched || control.dirty)"
|
||||||
|
(onInput)="onPriceInput($event)"
|
||||||
(onBlur)="blur.emit()"
|
(onBlur)="blur.emit()"
|
||||||
/>
|
/>
|
||||||
<!-- (input)="onInput($event)" -->
|
<!-- (input)="onInput($event)" -->
|
||||||
@@ -39,7 +40,9 @@
|
|||||||
(input)="onInput($event)"
|
(input)="onInput($event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@if (preparedSuffix()) {
|
@if (suffixTemp) {
|
||||||
|
<ng-container [ngTemplateOutlet]="suffixTemp" />
|
||||||
|
} @else if (preparedSuffix()) {
|
||||||
<p-inputgroup-addon>{{ preparedSuffix() }}</p-inputgroup-addon>
|
<p-inputgroup-addon>{{ preparedSuffix() }}</p-inputgroup-addon>
|
||||||
}
|
}
|
||||||
</p-inputgroup>
|
</p-inputgroup>
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ import { Maybe } from '@/core';
|
|||||||
import { ToastService } from '@/core/services/toast.service';
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, computed, EventEmitter, inject, Input, Output } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
ContentChild,
|
||||||
|
EventEmitter,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
TemplateRef,
|
||||||
|
} from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
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 { InputNumberModule } from 'primeng/inputnumber';
|
import { InputNumberInputEvent, 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';
|
||||||
@@ -33,6 +42,7 @@ export class InputComponent {
|
|||||||
@Input() type:
|
@Input() type:
|
||||||
| 'simple'
|
| 'simple'
|
||||||
| 'postalCode'
|
| 'postalCode'
|
||||||
|
| 'nationalId'
|
||||||
| 'mobile'
|
| 'mobile'
|
||||||
| 'phone'
|
| 'phone'
|
||||||
| 'email'
|
| 'email'
|
||||||
@@ -58,6 +68,8 @@ export class InputComponent {
|
|||||||
@Output() valueChange = new EventEmitter<string | number>();
|
@Output() valueChange = new EventEmitter<string | number>();
|
||||||
@Output() blur = new EventEmitter<void>();
|
@Output() blur = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
|
||||||
|
|
||||||
private readonly toastService = inject(ToastService);
|
private readonly toastService = inject(ToastService);
|
||||||
|
|
||||||
// onInput(ev: Event) {
|
// onInput(ev: Event) {
|
||||||
@@ -80,6 +92,8 @@ export class InputComponent {
|
|||||||
return '09xxxxxxxx';
|
return '09xxxxxxxx';
|
||||||
case 'postalCode':
|
case 'postalCode':
|
||||||
return 'کد پستی (۱۰ رقم)';
|
return 'کد پستی (۱۰ رقم)';
|
||||||
|
case 'nationalId':
|
||||||
|
return 'کد ملی (۱۰ رقم)';
|
||||||
case 'phone':
|
case 'phone':
|
||||||
return 'شماره تلفن';
|
return 'شماره تلفن';
|
||||||
case 'email':
|
case 'email':
|
||||||
@@ -96,6 +110,7 @@ export class InputComponent {
|
|||||||
case 'mobile':
|
case 'mobile':
|
||||||
case 'phone':
|
case 'phone':
|
||||||
case 'postalCode':
|
case 'postalCode':
|
||||||
|
case 'nationalId':
|
||||||
case 'price':
|
case 'price':
|
||||||
case 'number':
|
case 'number':
|
||||||
return 'numeric';
|
return 'numeric';
|
||||||
@@ -113,6 +128,7 @@ export class InputComponent {
|
|||||||
case 'mobile':
|
case 'mobile':
|
||||||
return 11;
|
return 11;
|
||||||
case 'postalCode':
|
case 'postalCode':
|
||||||
|
case 'nationalId':
|
||||||
return 10;
|
return 10;
|
||||||
case 'phone':
|
case 'phone':
|
||||||
return 11;
|
return 11;
|
||||||
@@ -132,6 +148,7 @@ export class InputComponent {
|
|||||||
break;
|
break;
|
||||||
case 'email':
|
case 'email':
|
||||||
case 'postalCode':
|
case 'postalCode':
|
||||||
|
case 'nationalId':
|
||||||
inputClass.push('ltrInput', 'rtlPlaceholder');
|
inputClass.push('ltrInput', 'rtlPlaceholder');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -150,6 +167,7 @@ export class InputComponent {
|
|||||||
case 'phone':
|
case 'phone':
|
||||||
case 'price':
|
case 'price':
|
||||||
case 'postalCode':
|
case 'postalCode':
|
||||||
|
case 'nationalId':
|
||||||
case 'number':
|
case 'number':
|
||||||
return 'number';
|
return 'number';
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
@@ -165,11 +183,22 @@ export class InputComponent {
|
|||||||
return this.required || this.control.hasValidator(Validators.required);
|
return this.required || this.control.hasValidator(Validators.required);
|
||||||
}
|
}
|
||||||
|
|
||||||
onInput($event: Event) {
|
onPriceInput($event: InputNumberInputEvent) {
|
||||||
|
console.log($event);
|
||||||
|
|
||||||
|
this.onInput($event.originalEvent, true);
|
||||||
|
}
|
||||||
|
onInput($event: Event, isPriceFormat?: boolean) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const value = $event.target.value as string;
|
let value = $event.target.value as string;
|
||||||
if (this.type === 'number') {
|
|
||||||
|
if (isPriceFormat) {
|
||||||
|
value = value.replace(/,/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inputMode === 'numeric') {
|
||||||
let newValue = parseFloat(value);
|
let newValue = parseFloat(value);
|
||||||
|
// console.log(value, newValue);
|
||||||
|
|
||||||
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
||||||
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
||||||
@@ -187,11 +216,21 @@ export class InputComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newValue = parseFloat(value.slice(0, value.length - 1));
|
newValue = parseFloat(value.slice(0, value.length - 1));
|
||||||
|
if (newValue < 0 || isNaN(newValue)) {
|
||||||
|
newValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPriceFormat) {
|
||||||
|
// @ts-ignore
|
||||||
|
$event.target.value = newValue.toLocaleString('en-US');
|
||||||
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
$event.target.value = newValue;
|
$event.target.value = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.control.setValue(newValue);
|
this.control.setValue(newValue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.preparedMaxLength && this.preparedMaxLength < value.length) {
|
if (this.preparedMaxLength && this.preparedMaxLength < value.length) {
|
||||||
let newValue = value.slice(0, value.length - 1);
|
let newValue = value.slice(0, value.length - 1);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
<div class="inline-flex gap-2 items-center w-full">
|
<div class="inline-flex flex-col gap-1 w-full">
|
||||||
<span class="text-muted-color text-base font-normal shrink-0">{{ label }}:</span>
|
<div class="inline-flex gap-2 items-center w-full">
|
||||||
|
<span class="text-muted-color text-base font-normal flex-auto grow-0">{{ label }}:</span>
|
||||||
|
<div class="shrink-0 grow">
|
||||||
<ng-content>
|
<ng-content>
|
||||||
<span class="text-text-color text-base font-bold grow">{{ value || "-" }}</span>
|
@if (valueType() === "badge") {
|
||||||
|
<p-badge [value]="valueToShow()?.toString()" [severity]="value ? 'success' : 'danger'" />
|
||||||
|
} @else {
|
||||||
|
<span class="text-text-color text-base font-bold grow"> {{ valueToShow() }}</span>
|
||||||
|
}
|
||||||
</ng-content>
|
</ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (hint) {
|
||||||
|
<span class="text-muted-color text-sm font-normal">{{ hint }}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { formatDurationToText, formatJalali, JALALI_DATE_FORMATS } from '@/utils';
|
||||||
|
import priceMaskUtils from '@/utils/price-mask.utils';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, computed, Input, signal } from '@angular/core';
|
||||||
|
import { Badge } from 'primeng/badge';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-key-value',
|
selector: 'app-key-value',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, Badge],
|
||||||
templateUrl: './key-value.component.html',
|
templateUrl: './key-value.component.html',
|
||||||
host: {
|
host: {
|
||||||
class: 'w-full block',
|
class: 'w-full block',
|
||||||
@@ -12,5 +15,86 @@ import { Component, Input } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class KeyValueComponent {
|
export class KeyValueComponent {
|
||||||
@Input() label!: string;
|
@Input() label!: string;
|
||||||
@Input() value?: string = '-';
|
@Input() value?: string | boolean | number;
|
||||||
|
@Input() hint?: string;
|
||||||
|
@Input() type:
|
||||||
|
| 'price'
|
||||||
|
| 'active'
|
||||||
|
| 'boolean'
|
||||||
|
| 'has'
|
||||||
|
| 'date'
|
||||||
|
| 'dateTime'
|
||||||
|
| 'duration'
|
||||||
|
| 'simple' = 'simple';
|
||||||
|
@Input() trueValueToShow?: string;
|
||||||
|
@Input() falseValueToShow?: string;
|
||||||
|
@Input() variant?: 'badge' | 'text';
|
||||||
|
|
||||||
|
valueToShow = signal(this.setValueToShow());
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.valueToShow.set(this.setValueToShow());
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueToShow() {
|
||||||
|
let value = this.value;
|
||||||
|
if (this.type !== 'simple') {
|
||||||
|
if (this.value) {
|
||||||
|
if (this.trueValueToShow) {
|
||||||
|
value = this.trueValueToShow;
|
||||||
|
} else {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'date':
|
||||||
|
if (typeof this.value === 'boolean') return '-';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return formatJalali(this.value);
|
||||||
|
case 'dateTime':
|
||||||
|
if (typeof this.value === 'boolean') return '-';
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
return formatJalali(this.value, JALALI_DATE_FORMATS.NUMERIC_WITH_TIME);
|
||||||
|
case 'duration':
|
||||||
|
return formatDurationToText(this.value as string);
|
||||||
|
case 'price':
|
||||||
|
return priceMaskUtils.formatWithCurrency(this.value as number);
|
||||||
|
case 'active':
|
||||||
|
value = 'فعال';
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
value = 'بله';
|
||||||
|
break;
|
||||||
|
case 'has':
|
||||||
|
value = 'دارد';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.falseValueToShow) {
|
||||||
|
value = this.falseValueToShow;
|
||||||
|
} else {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'active':
|
||||||
|
value = 'غیر فعال';
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
value = 'خیر';
|
||||||
|
break;
|
||||||
|
case 'has':
|
||||||
|
value = 'ندارد';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (this.type) {
|
||||||
|
case 'simple':
|
||||||
|
return this.value || '';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType = computed(() =>
|
||||||
|
this.variant || ['boolean', 'has', 'active'].includes(this.type) ? 'badge' : 'text',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const CustomerType = {
|
||||||
|
INDIVIDUAL: 'INDIVIDUAL',
|
||||||
|
LEGAL: 'LEGAL',
|
||||||
|
UNKNOWN: 'UNKNOWN',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CustomerType = (typeof CustomerType)[keyof typeof CustomerType];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ISelectItem } from '@/shared/abstractClasses';
|
||||||
|
|
||||||
|
export const goldKarat = {
|
||||||
|
KARAT_18: 'KARAT_18',
|
||||||
|
KARAT_21: 'KARAT_21',
|
||||||
|
KARAT_24: 'KARAT_24',
|
||||||
|
} as const;
|
||||||
|
export type TGoldKarat = (typeof goldKarat)[keyof typeof goldKarat];
|
||||||
|
|
||||||
|
const translates: Record<string, string> = {
|
||||||
|
KARAT_18: '18',
|
||||||
|
KARAT_21: '21',
|
||||||
|
KARAT_24: '24',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const goldKaratSelect: ISelectItem[] = Object.keys(goldKarat).map((key) => ({
|
||||||
|
id: key,
|
||||||
|
name: translates[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const goldKaratLabel = 'عیار';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { goldKaratLabel, goldKaratSelect } from './goldKarat';
|
||||||
|
|
||||||
|
export const LOCAL_ENUMS = {
|
||||||
|
gold: {
|
||||||
|
label: goldKaratLabel,
|
||||||
|
items: goldKaratSelect,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './io';
|
||||||
|
export * from './types';
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IEnumResponse {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import { LOCAL_ENUMS } from '../constants';
|
||||||
|
|
||||||
|
export type TLocalEnum = keyof typeof LOCAL_ENUMS;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<uikit-field [label]="label()" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
|
||||||
|
<p-select
|
||||||
|
[loading]="loading()"
|
||||||
|
[options]="items()"
|
||||||
|
optionLabel="name"
|
||||||
|
optionValue="value"
|
||||||
|
[formControl]="control"
|
||||||
|
[showClear]="showClear"
|
||||||
|
[filter]="true"
|
||||||
|
appendTo="body"
|
||||||
|
/>
|
||||||
|
</uikit-field>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { UikitFieldComponent } from '@/uikit';
|
||||||
|
import { Component, computed, Input } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Select } from 'primeng/select';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AbstractSelectComponent, ISelectItem } from '../abstractClasses';
|
||||||
|
import { LOCAL_ENUMS } from './constants';
|
||||||
|
import { TLocalEnum } from './models';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-enum-select',
|
||||||
|
templateUrl: './select.component.html',
|
||||||
|
imports: [UikitFieldComponent, Select, ReactiveFormsModule],
|
||||||
|
})
|
||||||
|
export class EnumSelectComponent extends AbstractSelectComponent<ISelectItem, ISelectItem[]> {
|
||||||
|
@Input() type!: TLocalEnum;
|
||||||
|
@Input() customLabel?: string;
|
||||||
|
|
||||||
|
label = computed(() => this.customLabel ?? LOCAL_ENUMS[this.type].label);
|
||||||
|
|
||||||
|
override getDataService() {
|
||||||
|
return new Observable<ISelectItem[]>((observer) => {
|
||||||
|
return observer.next(LOCAL_ENUMS[this.type].items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import jalaliPlugin from '@zoomit/dayjs-jalali-plugin';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import 'dayjs/locale/fa';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
// Extend dayjs with plugins
|
||||||
|
dayjs.extend(jalaliPlugin);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common date format patterns for Jalali calendar
|
||||||
|
*/
|
||||||
|
export const JALALI_DATE_FORMATS = {
|
||||||
|
/** Full date with weekday: "پنجشنبه، ۱۳ شهریور ۱۳۹۷" */
|
||||||
|
FULL_WITH_WEEKDAY: 'dddd، DD MMMM YYYY',
|
||||||
|
/** Full date: "۱۳ شهریور ۱۳۹۷" */
|
||||||
|
FULL: 'DD MMMM YYYY',
|
||||||
|
/** Short date with month name: "۱۳ شهریور ۹۷" */
|
||||||
|
SHORT: 'DD MMMM YY',
|
||||||
|
/** Numeric date: "۱۳۹۷/۰۶/۱۳" */
|
||||||
|
NUMERIC: 'YYYY/MM/DD',
|
||||||
|
/** Numeric date with time: "۱۳۹۷/۰۶/۱۳ ۱۶:۳۰" */
|
||||||
|
NUMERIC_WITH_TIME: 'YYYY/MM/DD HH:mm',
|
||||||
|
/** Numeric date with full time: "۱۳۹۷/۰۶/۱۳ ۱۶:۳۰:۱۵" */
|
||||||
|
NUMERIC_WITH_FULL_TIME: 'YYYY/MM/DD HH:mm:ss',
|
||||||
|
/** Time only: "۱۶:۳۰" */
|
||||||
|
TIME: 'HH:mm',
|
||||||
|
/** Time with seconds: "۱۶:۳۰:۱۵" */
|
||||||
|
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||||
|
/** Month and year: "شهریور ۱۳۹۷" */
|
||||||
|
MONTH_YEAR: 'MMMM YYYY',
|
||||||
|
/** Day and month: "۱۳ شهریور" */
|
||||||
|
DAY_MONTH: 'DD MMMM',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common date format patterns for Gregorian calendar
|
||||||
|
*/
|
||||||
|
export const GREGORIAN_DATE_FORMATS = {
|
||||||
|
/** Full date with weekday: "Thursday, September 04 2018" */
|
||||||
|
FULL_WITH_WEEKDAY: 'dddd, MMMM DD YYYY',
|
||||||
|
/** Full date: "September 04 2018" */
|
||||||
|
FULL: 'MMMM DD YYYY',
|
||||||
|
/** Short date: "Sep 04 18" */
|
||||||
|
SHORT: 'MMM DD YY',
|
||||||
|
/** Numeric date: "2018-09-04" */
|
||||||
|
NUMERIC: 'YYYY-MM-DD',
|
||||||
|
/** Numeric date with time: "2018-09-04 16:30" */
|
||||||
|
NUMERIC_WITH_TIME: 'YYYY-MM-DD HH:mm',
|
||||||
|
/** Numeric date with full time: "2018-09-04 16:30:15" */
|
||||||
|
NUMERIC_WITH_FULL_TIME: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
/** Time only: "16:30" */
|
||||||
|
TIME: 'HH:mm',
|
||||||
|
/** Time with seconds: "16:30:15" */
|
||||||
|
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||||
|
/** Month and year: "September 2018" */
|
||||||
|
MONTH_YEAR: 'MMMM YYYY',
|
||||||
|
/** Day and month: "September 04" */
|
||||||
|
DAY_MONTH: 'MMMM DD',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as Jalali (Persian) calendar in Farsi locale
|
||||||
|
*
|
||||||
|
* @param date - Date to format (string, Date, Dayjs, or timestamp)
|
||||||
|
* @param format - Format pattern (default: 'YYYY/MM/DD')
|
||||||
|
* @returns Formatted date string in Jalali calendar
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatJalali('2018-09-04') // '1397/06/13'
|
||||||
|
* formatJalali('2018-09-04', JALALI_DATE_FORMATS.FULL) // '۱۳ شهریور ۱۳۹۷'
|
||||||
|
* formatJalali(new Date(), JALALI_DATE_FORMATS.FULL_WITH_WEEKDAY) // 'پنجشنبه، ۱۳ شهریور ۱۳۹۷'
|
||||||
|
*/
|
||||||
|
export function formatJalali(
|
||||||
|
date: string | Date | Dayjs | number,
|
||||||
|
format: string = JALALI_DATE_FORMATS.NUMERIC,
|
||||||
|
): string {
|
||||||
|
return dayjs(date).calendar('jalali').locale('fa').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as Gregorian calendar in English locale
|
||||||
|
*
|
||||||
|
* @param date - Date to format (string, Date, Dayjs, or timestamp)
|
||||||
|
* @param format - Format pattern (default: 'YYYY-MM-DD')
|
||||||
|
* @returns Formatted date string in Gregorian calendar
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatGregorian('1397/06/13', GREGORIAN_DATE_FORMATS.FULL) // 'September 04 2018'
|
||||||
|
* formatGregorian(new Date(), GREGORIAN_DATE_FORMATS.NUMERIC_WITH_TIME) // '2018-09-04 16:30'
|
||||||
|
*/
|
||||||
|
export function formatGregorian(
|
||||||
|
date: string | Date | Dayjs | number,
|
||||||
|
format: string = GREGORIAN_DATE_FORMATS.NUMERIC,
|
||||||
|
): string {
|
||||||
|
return dayjs(date).calendar('gregory').locale('en').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a Jalali date string and return a Dayjs instance
|
||||||
|
*
|
||||||
|
* @param jalaliDate - Jalali date string (e.g., '1398-10-17')
|
||||||
|
* @param format - Optional format pattern for parsing
|
||||||
|
* @returns Dayjs instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseJalali('1398-10-17') // Dayjs instance
|
||||||
|
* parseJalali('1398/10/17') // Dayjs instance
|
||||||
|
*/
|
||||||
|
export function parseJalali(jalaliDate: string, format?: string): Dayjs {
|
||||||
|
return dayjs(jalaliDate, { jalali: true, ...(format && { format }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Gregorian date to Jalali format
|
||||||
|
*
|
||||||
|
* @param gregorianDate - Gregorian date string, Date object, or Dayjs instance
|
||||||
|
* @param format - Optional format pattern (default: 'YYYY/MM/DD')
|
||||||
|
* @returns Formatted Jalali date string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* gregorianToJalali('2018-09-04') // '1397/06/13'
|
||||||
|
* gregorianToJalali('2018-09-04', JALALI_DATE_FORMATS.FULL) // '۱۳ شهریور ۱۳۹۷'
|
||||||
|
*/
|
||||||
|
export function gregorianToJalali(
|
||||||
|
gregorianDate: string | Date | Dayjs,
|
||||||
|
format: string = JALALI_DATE_FORMATS.NUMERIC,
|
||||||
|
): string {
|
||||||
|
return dayjs(gregorianDate).calendar('jalali').locale('fa').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Jalali date to Gregorian format
|
||||||
|
*
|
||||||
|
* @param jalaliDate - Jalali date string (e.g., '1398-10-17')
|
||||||
|
* @param format - Optional format pattern (default: 'YYYY-MM-DD')
|
||||||
|
* @returns Formatted Gregorian date string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* jalaliToGregorian('1398-10-17') // '2020-01-07'
|
||||||
|
* jalaliToGregorian('1397/06/13', GREGORIAN_DATE_FORMATS.FULL) // 'September 04 2018'
|
||||||
|
*/
|
||||||
|
export function jalaliToGregorian(
|
||||||
|
jalaliDate: string,
|
||||||
|
format: string = GREGORIAN_DATE_FORMATS.NUMERIC,
|
||||||
|
): string {
|
||||||
|
return parseJalali(jalaliDate).calendar('gregory').locale('en').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current date and time in Jalali format
|
||||||
|
*
|
||||||
|
* @param format - Optional format pattern (default: 'YYYY/MM/DD HH:mm')
|
||||||
|
* @returns Current date and time in Jalali format
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* nowJalali() // '1403/12/05 14:30'
|
||||||
|
* nowJalali(JALALI_DATE_FORMATS.FULL_WITH_WEEKDAY) // 'یکشنبه، ۵ اسفند ۱۴۰۳'
|
||||||
|
*/
|
||||||
|
export function nowJalali(format: string = JALALI_DATE_FORMATS.NUMERIC_WITH_TIME): string {
|
||||||
|
return dayjs().calendar('jalali').locale('fa').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current date and time in Gregorian format
|
||||||
|
*
|
||||||
|
* @param format - Optional format pattern (default: 'YYYY-MM-DD HH:mm')
|
||||||
|
* @returns Current date and time in Gregorian format
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* nowGregorian() // '2026-02-24 14:30'
|
||||||
|
* nowGregorian(GREGORIAN_DATE_FORMATS.FULL_WITH_WEEKDAY) // 'Sunday, February 24 2026'
|
||||||
|
*/
|
||||||
|
export function nowGregorian(format: string = GREGORIAN_DATE_FORMATS.NUMERIC_WITH_TIME): string {
|
||||||
|
return dayjs().calendar('gregory').locale('en').format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a time duration as a relative time string in Farsi
|
||||||
|
*
|
||||||
|
* @param date - Date to compare against current time
|
||||||
|
* @returns Relative time string in Farsi
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* relativeTimeJalali(dayjs().subtract(2, 'day')) // '۲ روز پیش'
|
||||||
|
*/
|
||||||
|
export function relativeTimeJalali(date: string | Date | Dayjs | number): string {
|
||||||
|
return dayjs(date).locale('fa').fromNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date string is a valid Jalali date
|
||||||
|
*
|
||||||
|
* @param jalaliDate - Jalali date string
|
||||||
|
* @returns true if valid, false otherwise
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* isValidJalaliDate('1398-10-17') // true
|
||||||
|
* isValidJalaliDate('1398-13-17') // false
|
||||||
|
*/
|
||||||
|
export function isValidJalaliDate(jalaliDate: string): boolean {
|
||||||
|
return parseJalali(jalaliDate).isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the start of day for a date in Jalali calendar
|
||||||
|
*
|
||||||
|
* @param date - Date to get start of day for
|
||||||
|
* @returns Dayjs instance at start of day
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* startOfDayJalali('1398-10-17') // Dayjs instance at 00:00:00
|
||||||
|
*/
|
||||||
|
export function startOfDayJalali(date: string | Date | Dayjs | number): Dayjs {
|
||||||
|
return dayjs(date).calendar('jalali').startOf('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the end of day for a date in Jalali calendar
|
||||||
|
*
|
||||||
|
* @param date - Date to get end of day for
|
||||||
|
* @returns Dayjs instance at end of day
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* endOfDayJalali('1398-10-17') // Dayjs instance at 23:59:59
|
||||||
|
*/
|
||||||
|
export function endOfDayJalali(date: string | Date | Dayjs | number): Dayjs {
|
||||||
|
return dayjs(date).calendar('jalali').endOf('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date with custom calendar and locale
|
||||||
|
*
|
||||||
|
* @param date - Date to format
|
||||||
|
* @param calendar - Calendar type ('jalali' | 'gregory')
|
||||||
|
* @param locale - Locale ('fa' | 'en')
|
||||||
|
* @param format - Format pattern
|
||||||
|
* @returns Formatted date string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatDate('2018-09-04', 'jalali', 'fa', 'DD MMMM YYYY') // '۱۳ شهریور ۱۳۹۷'
|
||||||
|
* formatDate('2018-09-04', 'gregory', 'en', 'MMMM DD YYYY') // 'September 04 2018'
|
||||||
|
*/
|
||||||
|
export function formatDate(
|
||||||
|
date: string | Date | Dayjs | number,
|
||||||
|
calendar: 'jalali' | 'gregory',
|
||||||
|
locale: 'fa' | 'en',
|
||||||
|
format: string,
|
||||||
|
): string {
|
||||||
|
return dayjs(date).calendar(calendar).locale(locale).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Jalali month name in Farsi
|
||||||
|
*
|
||||||
|
* @param monthNumber - Month number (1-12)
|
||||||
|
* @returns Month name in Farsi
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getJalaliMonthName(1) // 'فروردین'
|
||||||
|
* getJalaliMonthName(6) // 'شهریور'
|
||||||
|
*/
|
||||||
|
export function getJalaliMonthName(monthNumber: number): string {
|
||||||
|
const monthNames = [
|
||||||
|
'فروردین',
|
||||||
|
'اردیبهشت',
|
||||||
|
'خرداد',
|
||||||
|
'تیر',
|
||||||
|
'مرداد',
|
||||||
|
'شهریور',
|
||||||
|
'مهر',
|
||||||
|
'آبان',
|
||||||
|
'آذر',
|
||||||
|
'دی',
|
||||||
|
'بهمن',
|
||||||
|
'اسفند',
|
||||||
|
];
|
||||||
|
return monthNames[monthNumber - 1] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Jalali weekday name in Farsi
|
||||||
|
*
|
||||||
|
* @param dayNumber - Day number (0-6, where 0 is Saturday)
|
||||||
|
* @returns Weekday name in Farsi
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getJalaliWeekdayName(0) // 'شنبه'
|
||||||
|
* getJalaliWeekdayName(4) // 'چهارشنبه'
|
||||||
|
*/
|
||||||
|
export function getJalaliWeekdayName(dayNumber: number): string {
|
||||||
|
const weekdayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'];
|
||||||
|
return weekdayNames[dayNumber] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jalaliDiff(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
unit: dayjs.OpUnitType = 'day',
|
||||||
|
): number {
|
||||||
|
return toJalali(date1).diff(toJalali(date2), unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJalaliBefore(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
): boolean {
|
||||||
|
return toJalali(date1).isBefore(toJalali(date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJalaliAfter(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
): boolean {
|
||||||
|
return toJalali(date1).isAfter(toJalali(date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): Dayjs {
|
||||||
|
return dayjs(date).calendar('jalali');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
formatJalali,
|
||||||
|
formatGregorian,
|
||||||
|
parseJalali,
|
||||||
|
gregorianToJalali,
|
||||||
|
jalaliToGregorian,
|
||||||
|
nowJalali,
|
||||||
|
nowGregorian,
|
||||||
|
relativeTimeJalali,
|
||||||
|
isValidJalaliDate,
|
||||||
|
startOfDayJalali,
|
||||||
|
endOfDayJalali,
|
||||||
|
formatDate,
|
||||||
|
getJalaliMonthName,
|
||||||
|
getJalaliWeekdayName,
|
||||||
|
jalaliDiff,
|
||||||
|
isJalaliBefore,
|
||||||
|
isJalaliAfter,
|
||||||
|
JALALI_DATE_FORMATS,
|
||||||
|
GREGORIAN_DATE_FORMATS,
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export const UnitType = {
|
||||||
|
COUNT: 'COUNT',
|
||||||
|
GRAM: 'GRAM',
|
||||||
|
KILOGRAM: 'KILOGRAM',
|
||||||
|
MILLILITER: 'MILLILITER',
|
||||||
|
LITER: 'LITER',
|
||||||
|
METER: 'METER',
|
||||||
|
HOUR: 'HOUR',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type UnitType = (typeof UnitType)[keyof typeof UnitType];
|
||||||
|
|
||||||
|
export interface IUnitPreparedText {
|
||||||
|
quantitySymbolText: string;
|
||||||
|
quantityLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translates: Record<UnitType, string> = {
|
||||||
|
COUNT: 'تعداد',
|
||||||
|
GRAM: 'گرم',
|
||||||
|
KILOGRAM: 'کیلوگرم',
|
||||||
|
METER: 'متر',
|
||||||
|
MILLILITER: 'میلیلیتر',
|
||||||
|
LITER: 'لیتر',
|
||||||
|
HOUR: 'ساعت',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGoodUnitTypeProperties(unitType: UnitType): IUnitPreparedText {
|
||||||
|
const data = {
|
||||||
|
quantitySymbolText: translates[unitType],
|
||||||
|
quantityLabel: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (['COUNT'].includes(unitType)) {
|
||||||
|
data.quantityLabel = 'تعداد';
|
||||||
|
} else {
|
||||||
|
data.quantityLabel = 'مقدار';
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
export * from './currency';
|
export * from './currency';
|
||||||
export * from './jalali-date.utils';
|
export * from './date-formatter.utils';
|
||||||
|
export * from './good-unit-types.utils';
|
||||||
export * from './name';
|
export * from './name';
|
||||||
export * from './page-params.utils';
|
export * from './page-params.utils';
|
||||||
export * from './price-mask.utils';
|
export * from './price-mask.utils';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
|
export * from './uuid.utils';
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import dayjs, { Dayjs } from 'dayjs';
|
|
||||||
import jalaliday from 'jalaliday';
|
|
||||||
|
|
||||||
dayjs.extend(jalaliday);
|
|
||||||
|
|
||||||
export function toJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): Dayjs {
|
|
||||||
return dayjs(date).calendar('jalali');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromJalali(jalaliDate: string): Dayjs {
|
|
||||||
// Parse Jalali date string (e.g., '1404-09-21')
|
|
||||||
return dayjs(jalaliDate, { jalali: true }).calendar('gregory');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): string {
|
|
||||||
return toJalali(date).locale('fa').format(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jalaliDiff(
|
|
||||||
date1: string | number | Date | Dayjs,
|
|
||||||
date2: string | number | Date | Dayjs,
|
|
||||||
unit: dayjs.OpUnitType = 'day',
|
|
||||||
): number {
|
|
||||||
return toJalali(date1).diff(toJalali(date2), unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isJalaliBefore(
|
|
||||||
date1: string | number | Date | Dayjs,
|
|
||||||
date2: string | number | Date | Dayjs,
|
|
||||||
): boolean {
|
|
||||||
return toJalali(date1).isBefore(toJalali(date2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isJalaliAfter(
|
|
||||||
date1: string | number | Date | Dayjs,
|
|
||||||
date2: string | number | Date | Dayjs,
|
|
||||||
): boolean {
|
|
||||||
return toJalali(date1).isAfter(toJalali(date2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nowJalali(): Dayjs {
|
|
||||||
return dayjs().calendar('jalali');
|
|
||||||
}
|
|
||||||
+31
-1
@@ -10,4 +10,34 @@ export function timeToSeconds(time: string): number {
|
|||||||
return hours * 3600 + minutes * 60 + seconds;
|
return hours * 3600 + minutes * 60 + seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { timeToSeconds };
|
/**
|
||||||
|
* Format duration string to Persian format
|
||||||
|
* Returns formatted string with hours, minutes, and/or seconds
|
||||||
|
* Excludes zero values from the output
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* formatDurationToText("01:30:00") -> "1 ساعت و 30 دقیقه"
|
||||||
|
* formatDurationToText("00:30:00") -> "30 دقیقه"
|
||||||
|
* formatDurationToText("01:00:00") -> "1 ساعت"
|
||||||
|
* formatDurationToText("00:00:30") -> "30 ثانیه"
|
||||||
|
*/
|
||||||
|
export function formatDurationToText(time: string): string {
|
||||||
|
const [hours, minutes, seconds] = time.split(':').map(Number);
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
parts.push(`${hours} ساعت`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
parts.push(`${minutes} دقیقه`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 0) {
|
||||||
|
parts.push(`${seconds} ثانیه`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' و ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { timeToSeconds, formatDurationToText };
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const createUUID = () => Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||||
@@ -47,3 +47,14 @@ input.p-inputtext {
|
|||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-inline-end: 0.5rem;
|
margin-inline-end: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-inputgroup > .p-component:first-child {
|
||||||
|
border-start-start-radius: 0 !important;
|
||||||
|
border-end-start-radius: 0 !important;
|
||||||
|
border-end-end-radius: var(--p-inputgroup-addon-border-radius);
|
||||||
|
border-start-end-radius: var(--p-inputgroup-addon-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-tablist {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,3 +7,9 @@
|
|||||||
@use "primeicons/primeicons.css";
|
@use "primeicons/primeicons.css";
|
||||||
@use "./demo/demo.scss";
|
@use "./demo/demo.scss";
|
||||||
@use "./rtlSupport.scss";
|
@use "./rtlSupport.scss";
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user