update pos consumer
This commit is contained in:
@@ -1,5 +1,23 @@
|
||||
@if (loading()) {
|
||||
<shared-page-loading />
|
||||
} @else {
|
||||
<router-outlet></router-outlet>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import { AuthService } from '@/core';
|
||||
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
||||
import { JalaliDateDirective } from '@/shared/directives';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import images from 'src/assets/images';
|
||||
import { PosStore } from '../pos.store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-layout',
|
||||
templateUrl: './layout.component.html',
|
||||
imports: [PageLoadingComponent, RouterOutlet],
|
||||
imports: [PageLoadingComponent, RouterOutlet, JalaliDateDirective],
|
||||
})
|
||||
export class PosLayoutComponent {
|
||||
constructor() {}
|
||||
|
||||
private readonly store = inject(PosStore);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
readonly loading = computed(() => this.store.loading());
|
||||
readonly posInfo = computed(() => this.store.entity());
|
||||
readonly userInfo = computed(() => this.authService.currentAccount());
|
||||
|
||||
placeholderLogo = images.placeholders.logo;
|
||||
|
||||
now = new Date();
|
||||
|
||||
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 = {
|
||||
info: () => `${baseUrl}`,
|
||||
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 './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 {
|
||||
mobile_number: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
fullname: string;
|
||||
id: string;
|
||||
role: TRoles;
|
||||
export interface IPosOrderRequest {
|
||||
total_amount: number;
|
||||
invoice_date: string;
|
||||
payments: IPayment;
|
||||
items: IPosOrderItem[];
|
||||
notes?: string;
|
||||
customer_type?: CustomerType;
|
||||
customerId?: string;
|
||||
customer?: ICustomer;
|
||||
}
|
||||
export interface IUserResponse extends IUserRawResponse {}
|
||||
|
||||
export interface IUserRequest {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
mobile_number: string;
|
||||
role: TRoles;
|
||||
}
|
||||
export interface IPosOrderRawResponse {}
|
||||
export interface IPosOrderResponse extends IPosOrderRawResponse {}
|
||||
|
||||
@@ -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 { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { POS_API_ROUTES } from '../constants';
|
||||
import { IUserRawResponse, IUserResponse } from '../models';
|
||||
import { IPosOrderRawResponse, IPosOrderRequest, IPosOrderResponse } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PosService {
|
||||
@@ -14,7 +21,17 @@ export class PosService {
|
||||
getInfo(): Observable<IPosInfoResponse> {
|
||||
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
|
||||
}
|
||||
getGoods(): Observable<IUserResponse> {
|
||||
return this.http.get<IUserRawResponse>(this.apiRoutes.goods());
|
||||
getGoods(searchQuery: string): Observable<IListingResponse<IGoodResponse>> {
|
||||
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 { PosStore } from '@/domains/pos/pos.store';
|
||||
import { LayoutService } from '@/layout/service/layout.service';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { PosGoodsComponent } from '../components/goods.component';
|
||||
import { PosOrderSectionComponent } from '../components/order/order-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-landing',
|
||||
templateUrl: './root.component.html',
|
||||
host: { class: 'grow overflow-hidden' },
|
||||
|
||||
imports: [PosGoodsComponent, PosOrderSectionComponent],
|
||||
})
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class PosLandingComponent {}
|
||||
|
||||
Reference in New Issue
Block a user