feat: optimize performance with lazy loading and change detection improvements
chore: update build configurations and remove unused styles fix: enhance user experience with loading indicators and improved asset management
This commit is contained in:
+10
-4
@@ -55,13 +55,13 @@
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"maximumError": "3MB",
|
||||
"maximumWarning": "2MB",
|
||||
"maximumError": "2.5MB",
|
||||
"maximumWarning": "1.5MB",
|
||||
"type": "initial"
|
||||
},
|
||||
{
|
||||
"maximumError": "8kB",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "6kB",
|
||||
"maximumWarning": "3kB",
|
||||
"type": "anyComponentStyle"
|
||||
}
|
||||
],
|
||||
@@ -136,6 +136,12 @@
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"dayjs",
|
||||
"dayjs/locale/fa",
|
||||
"dayjs/plugin/relativeTime",
|
||||
"flatpickr-wrap/dist/l10n/fa.js"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"appData": {
|
||||
"appVersion": "0.0.0",
|
||||
"buildDate": "2026-05-04T18:58:47.135Z"
|
||||
"buildDate": "2026-05-10T09:04:30.388Z"
|
||||
},
|
||||
"assetGroups": [
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 524 KiB |
@@ -1,10 +1,11 @@
|
||||
import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, 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',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [Skeleton],
|
||||
})
|
||||
export class PosGoodCategoriesComponent {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedGoodToAdd()) {
|
||||
@defer (when showPayloadForm()) {
|
||||
<pos-payload-form-dialog
|
||||
[(visible)]="showPayloadForm"
|
||||
[good]="selectedGoodToAdd()!"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { ChangeDetectionStrategy, Component, computed, inject, Input, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Divider } from 'primeng/divider';
|
||||
import { SelectButton } from 'primeng/selectbutton';
|
||||
@@ -14,6 +14,7 @@ import { PayloadFormDialogComponent } from './payloads';
|
||||
@Component({
|
||||
selector: 'pos-goods',
|
||||
templateUrl: './goods.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
PosGoodCategoriesComponent,
|
||||
PosGoodsListViewComponent,
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
@for (good of goods(); track good.id) {
|
||||
@for (good of visibleGoods(); track good.id) {
|
||||
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs" (click)="addProduct(good)">
|
||||
<img [src]="good.image_url || goodPlaceholder" class="w-full aspect-[1.3] object-cover rounded-md" />
|
||||
<img
|
||||
[src]="good.image_url || goodPlaceholder"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full aspect-[1.3] object-cover rounded-md"
|
||||
/>
|
||||
<div class="mt-2 text-center">
|
||||
<span class="text-lg font-bold text-center">
|
||||
{{ good.name }}
|
||||
@@ -25,3 +30,8 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (!loading() && hasMore()) {
|
||||
<div class="flex justify-center mt-3">
|
||||
<button pButton type="button" outlined (click)="loadMore()">نمایش کالاهای بیشتر</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||
import { Component, computed, EventEmitter, inject, Output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import images from 'src/assets/images';
|
||||
@@ -8,6 +17,7 @@ import { PosLandingStore } from '../store/main.store';
|
||||
@Component({
|
||||
selector: 'pos-good-grid-view',
|
||||
templateUrl: './grid-view.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonDirective, Skeleton],
|
||||
})
|
||||
export class PosGoodsGridViewComponent {
|
||||
@@ -15,6 +25,10 @@ export class PosGoodsGridViewComponent {
|
||||
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||
|
||||
goods = computed(() => this.store.filteredGoods());
|
||||
private readonly pageSize = 80;
|
||||
readonly renderLimit = signal(this.pageSize);
|
||||
visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit()));
|
||||
hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0));
|
||||
loading = computed(() => this.store.getGoodsLoading());
|
||||
|
||||
goodPlaceholder = images.placeholders.default;
|
||||
@@ -22,4 +36,15 @@ export class PosGoodsGridViewComponent {
|
||||
addProduct(good: IGoodResponse) {
|
||||
this.onAdd.emit(good);
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.renderLimit.update((limit) => limit + this.pageSize);
|
||||
}
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onWindowScroll() {
|
||||
if (!this.hasMore()) return;
|
||||
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
|
||||
if (nearBottom) this.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
@for (good of goods(); track good.id) {
|
||||
@for (good of visibleGoods(); track good.id) {
|
||||
<div class="flex items-center bg-surface-card rounded-lg p-1 shadow-xs gap-2">
|
||||
<div class="grow flex items-center gap-2">
|
||||
<img [src]="good.image_url || goodPlaceholder" class="w-12 h-12 object-cover rounded-md" />
|
||||
<img
|
||||
[src]="good.image_url || goodPlaceholder"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-12 h-12 object-cover rounded-md"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold">
|
||||
{{ good.name }}
|
||||
@if (!good.is_default_guild_good) {
|
||||
<small class="text-xs text-muted-color">*</small>
|
||||
}
|
||||
<!-- <small class="text-xs text-muted-color">({{ good.quantity }})</small> -->
|
||||
</span>
|
||||
<span class="text-sm text-muted-color">
|
||||
{{ good.sku }}
|
||||
@@ -24,10 +28,12 @@
|
||||
</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>
|
||||
}
|
||||
@if (hasMore()) {
|
||||
<button pButton type="button" outlined class="mx-auto" (click)="loadMore()">نمایش کالاهای بیشتر</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { IGoodResponse } from '@/domains/pos/models/good.io';
|
||||
import { Component, computed, EventEmitter, inject, Output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import images from 'src/assets/images';
|
||||
@@ -8,6 +17,7 @@ import { PosLandingStore } from '../store/main.store';
|
||||
@Component({
|
||||
selector: 'pos-goods-list-view',
|
||||
templateUrl: './list-view.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonDirective, Skeleton],
|
||||
})
|
||||
export class PosGoodsListViewComponent {
|
||||
@@ -15,6 +25,10 @@ export class PosGoodsListViewComponent {
|
||||
@Output() onAdd = new EventEmitter<IGoodResponse>();
|
||||
|
||||
goods = computed(() => this.store.filteredGoods());
|
||||
private readonly pageSize = 120;
|
||||
readonly renderLimit = signal(this.pageSize);
|
||||
visibleGoods = computed(() => this.goods()?.slice(0, this.renderLimit()));
|
||||
hasMore = computed(() => (this.goods()?.length || 0) > (this.visibleGoods()?.length || 0));
|
||||
loading = computed(() => this.store.getGoodsLoading());
|
||||
|
||||
goodPlaceholder = images.placeholders.default;
|
||||
@@ -22,4 +36,15 @@ export class PosGoodsListViewComponent {
|
||||
addGood(good: IGoodResponse) {
|
||||
this.onAdd.emit(good);
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.renderLimit.update((limit) => limit + this.pageSize);
|
||||
}
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onWindowScroll() {
|
||||
if (!this.hasMore()) return;
|
||||
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
|
||||
if (nearBottom) this.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
<div class="flex gap-3 items-center">
|
||||
<img
|
||||
[src]="inOrderGood.good.image_url || placeholderImage"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-24 h-24 rounded-md bg-surface-100 object-cover shrink-0 border border-surface-border"
|
||||
/>
|
||||
<div class="flex flex-col grow justify-between overflow-hidden">
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { PriceMaskDirective } from '@/shared/directives';
|
||||
import { enumTranslator } from '@/utils';
|
||||
import { Component, inject, Input, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, Input, signal } from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import images from 'src/assets/images';
|
||||
import { IPosInOrderGood } from '../../models';
|
||||
@@ -10,6 +10,7 @@ import { PayloadFormDialogComponent } from '../payload-form.component';
|
||||
@Component({
|
||||
selector: 'pos-order-card-template',
|
||||
templateUrl: './order-card-template.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonDirective, PayloadFormDialogComponent, PriceMaskDirective],
|
||||
})
|
||||
export class OrderCardTemplateComponent {
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
|
||||
@defer (when isVisibleCustomerForm()) {
|
||||
<pos-order-customer-dialog [(visible)]="isVisibleCustomerForm" (onSubmit)="submitCustomer()" />
|
||||
}
|
||||
<!-- <pos-pre-invoice-dialog [(visible)]="isVisiblePreInvoiceForm" /> -->
|
||||
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
|
||||
@defer (when isVisiblePaymentForm()) {
|
||||
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
|
||||
// import { ICustomerResponse } from '@/modules/customers/models';
|
||||
import { KeyValueComponent } from '@/shared/components';
|
||||
import { Component, computed, EventEmitter, inject, Output, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, EventEmitter, inject, Output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
@@ -17,6 +17,7 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
|
||||
@Component({
|
||||
selector: 'pos-order-section',
|
||||
templateUrl: './order-section.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ButtonDirective,
|
||||
POSOrderPriceInfoCardComponent,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AmountPercentageInputComponent } from '@/shared/components/amountPercen
|
||||
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
|
||||
import { formatWithCurrency } from '@/utils';
|
||||
import { Component, computed, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton';
|
||||
@@ -71,35 +72,19 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
|
||||
}),
|
||||
});
|
||||
|
||||
form.valueChanges.subscribe((value) => {
|
||||
form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.updateCalculateAmount(value as any);
|
||||
});
|
||||
|
||||
form.controls.payload.controls.profit_amount.valueChanges.subscribe((value) => {
|
||||
form.controls.payload.controls.profit_amount.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
// if ((form.controls.discount_amount.value || 0) > (value || 0)) {
|
||||
form.controls.discount_amount.setValue(0);
|
||||
// }
|
||||
});
|
||||
form.controls.payload.controls.profit_percentage.valueChanges.subscribe((value) => {
|
||||
form.controls.payload.controls.profit_percentage.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
form.controls.discount_amount.setValue(0);
|
||||
});
|
||||
|
||||
form.setValue({
|
||||
unit_price: 200_000_000,
|
||||
quantity: 2,
|
||||
discount_amount: 0,
|
||||
discount_percentage: 0,
|
||||
payload: {
|
||||
commission_amount: 0,
|
||||
commission_percentage: 10,
|
||||
wages_amount: 0,
|
||||
wages_percentage: 10,
|
||||
profit_amount: 0,
|
||||
profit_percentage: 10,
|
||||
karat: '18',
|
||||
},
|
||||
});
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InputComponent } from '@/shared/components';
|
||||
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
|
||||
import { formatWithCurrency, getGoodUnitTypeProperties, UnitType } from '@/utils';
|
||||
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Button } from 'primeng/button';
|
||||
import { IPosOrderItem, IStandardPayload } from '../../../models';
|
||||
@@ -34,7 +35,7 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
|
||||
discount_percentage: [0, [Validators.min(0), Validators.max(100)]],
|
||||
});
|
||||
|
||||
form.valueChanges.subscribe((value) => {
|
||||
form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.updateCalculateAmount(value as Partial<IPosOrderItem<IStandardPayload>>);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SharedDialogComponent } from '@/shared/components/dialog/dialog.compone
|
||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||
import { UikitFieldComponent } from '@/uikit';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Select } from 'primeng/select';
|
||||
@@ -74,7 +75,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
||||
terminals: this.fb.array([this.fb.control(0)]),
|
||||
});
|
||||
|
||||
form.valueChanges.subscribe((value) => {
|
||||
form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
const terminalTotal =
|
||||
(value.terminals || []).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-
|
||||
import { SharedDialogComponent } from '@/shared/components';
|
||||
import { PageLoadingComponent } from '@/shared/components/page-loading.component';
|
||||
import { PriceMaskDirective } from '@/shared/directives';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { PosGoodsComponent } from '../components/goods.component';
|
||||
import { PosOrderSectionComponent } from '../components/order/order-section.component';
|
||||
@@ -17,6 +17,7 @@ import { PosLandingStore } from '../store/main.store';
|
||||
selector: 'pos-landing',
|
||||
templateUrl: './root.component.html',
|
||||
host: { class: 'grow overflow-hidden' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
imports: [
|
||||
PosGoodsComponent,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
|
||||
import { InputComponent } from '@/shared/components';
|
||||
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -28,7 +27,6 @@ import { IPosSaleInvoicesFilterDto } from '../models';
|
||||
ButtonDirective,
|
||||
EnumSelectComponent,
|
||||
InputComponent,
|
||||
UikitFlatpickrJalaliComponent,
|
||||
],
|
||||
})
|
||||
export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { BreadcrumbComponent } from '@/shared/components';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, ContentChild, inject, Renderer2, TemplateRef } from '@angular/core';
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||
import { filter, Subscription } from 'rxjs';
|
||||
import { LayoutService } from '../service/layout.service';
|
||||
import { AppFooter } from './app.footer.component';
|
||||
@@ -14,15 +13,7 @@ import { AppTopbar } from './app.topbar.component';
|
||||
@Component({
|
||||
selector: 'app-layout',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppTopbar,
|
||||
AppSidebar,
|
||||
RouterModule,
|
||||
AppFooter,
|
||||
BreadcrumbComponent,
|
||||
ProgressSpinner,
|
||||
],
|
||||
imports: [CommonModule, AppTopbar, AppSidebar, RouterModule, AppFooter, BreadcrumbComponent],
|
||||
templateUrl: './app.layout.component.html',
|
||||
})
|
||||
export class AppLayout {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@use "./flatpicker.css";
|
||||
@use "./layout/layout.scss";
|
||||
@use "primeicons/primeicons.css";
|
||||
@use "./demo/demo.scss";
|
||||
@use "./rtlSupport.scss";
|
||||
@use "./customize.scss";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user