feat(pos): add shop and statistics modules with components, services, and routing

- Implemented shop module with root component, loading state, and invoice handling.
- Created statistics module with invoice type card component, API routes, and data models.
- Added season picker component for selecting year and season.
- Integrated services for fetching statistics data and managing state.
- Established routing for statistics and shop views.
This commit is contained in:
2026-05-23 18:09:44 +03:30
parent 8c07dc7c3f
commit 6ad1a73c16
100 changed files with 768 additions and 231 deletions
@@ -1,5 +1,5 @@
import ISummary from '@/core/models/summary';
import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/landing/models';
import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/shop/models';
export interface ISalesInvoicesRawResponse {
id: string;
@@ -5,11 +5,12 @@ import { RouterLink } from '@angular/router';
import { Button } from 'primeng/button';
import { Drawer } from 'primeng/drawer';
import { Ripple } from 'primeng/ripple';
import config from 'src/config';
import { posAboutNamedRoutes } from '../../modules/about/constants';
import { posConfigNamedRoutes } from '../../modules/configs/constants';
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/constants';
import { PosShopNamedRoutes } from '../../modules/shop/constants/routes';
import { StatisticsRoutes } from '../../modules/statistics/constants';
import { posSupportNamedRoutes } from '../../modules/support/constants';
import { PosInfoStore } from '../../store';
@@ -37,10 +38,15 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
readonly menuItems = [
{
label: 'فروش',
routerLink: config.isPosApplication ? '/' : '/pos',
label: 'صفحه اصلی',
routerLink: StatisticsRoutes.statistics.meta.pagePath!(),
icon: 'pi pi-fw pi-home',
},
{
label: 'فروش',
routerLink: PosShopNamedRoutes.shop.meta.pagePath!(),
icon: 'pi pi-fw pi-cart-arrow-down',
},
{
label: 'فاکتورها',
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
@@ -9,7 +9,7 @@ import { Message } from 'primeng/message';
import { catchError, finalize } from 'rxjs';
import { COOKIE_KEYS } from 'src/assets/constants';
import { IPosAccessibleResponse } from '../../models/pos.io';
import { PosService } from '../../modules/landing/services/main.service';
import { PosService } from '../../modules/shop/services/main.service';
@Component({
selector: 'pos-choose-cards',
@@ -35,7 +35,7 @@ export class PosChooseCardsComponent {
catchError((err) => {
this.error.set(err);
throw err;
}),
})
)
.subscribe((res) => {
this.items.set(res?.data || []);
@@ -1,8 +1,8 @@
<div class="w-full h-full flex items-center justify-center p-4">
<div class="flex h-full w-full items-center justify-center p-4">
<app-card-data cardTitle="تنظیمات فاکتور" class="w-full">
<p-message variant="text" severity="info" icon="pi pi-info-circle">
هریک از موارد زیر را که می‌خواهید در چاپ فاکتور نمایش داده‌ شوند را انتخاب کنید
</p-message>
<pos-config-print-form class="block w-full mt-6" (onClose)="returnToMainPage()" (onSubmit)="returnToMainPage()" />
<pos-config-print-form class="mt-6 block w-full" (onClose)="returnToMainPage()" (onSubmit)="returnToMainPage()" />
</app-card-data>
</div>
@@ -1,4 +1,5 @@
import { AppCardComponent } from '@/shared/components';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Message } from 'primeng/message';
@@ -7,7 +8,7 @@ import { PosConfigPrintFormComponent } from '../components/print/form.component'
@Component({
selector: 'pos-config-page',
templateUrl: './root.component.html',
imports: [PosConfigPrintFormComponent, AppCardComponent, Message],
imports: [PosConfigPrintFormComponent, AppCardComponent, Message, UikitFlatpickrJalaliComponent],
})
export class PosConfigPageComponent {
private readonly router = inject(Router);
@@ -1,28 +0,0 @@
<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>
@@ -1,57 +0,0 @@
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);
}
}
@@ -1,19 +0,0 @@
<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>
@@ -1,39 +0,0 @@
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,7 +50,7 @@ export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
customer_mobile: this.fb.control<string>(''),
customer_national_id: this.fb.control<string>(''),
customer_economic_code: this.fb.control<string>(''),
status: this.fb.control<string | null>(null),
status: this.fb.control<'NOT_SEND' | 'SUCCESS' | 'FAILURE' | 'QUEUED' | null>(null),
total_amount_from: this.fb.control<number | null>(null),
total_amount_to: this.fb.control<number | null>(null),
});
@@ -74,7 +74,7 @@ export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
total_amount_from: this.value.total_amount_from ?? null,
total_amount_to: this.value.total_amount_to ?? null,
},
{ emitEvent: false },
{ emitEvent: false }
);
}
}
@@ -1,7 +1,8 @@
import { POS_ROUTES } from '@/domains/pos/routes';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { gregorianToJalali } from '@/utils';
import { Component, computed, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { Chip } from 'primeng/chip';
import { Skeleton } from 'primeng/skeleton';
@@ -32,6 +33,7 @@ import { SaleInvoiceCardComponent } from './sale-invoice-card.component';
export class PosSaleInvoiceListComponent {
private readonly service = inject(PosSaleInvoicesService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
loading = signal(true);
items = signal<IPosSaleInvoicesSummaryResponse[]>([]);
@@ -57,16 +59,28 @@ export class PosSaleInvoiceListComponent {
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => {
const typedKey = key as keyof IPosSaleInvoicesFilterDto;
const formattedValue =
typedKey === 'status' && value === 'NOT_SEND'
? 'ارسال نشده'
: typedKey === 'status' && value === 'SUCCESS'
? 'موفق'
: typedKey === 'status' && value === 'FAILURE'
? 'ناموفق'
: typedKey === 'status' && value === 'QUEUED'
? 'در انتظار ارسال'
: String(value);
let formattedValue = '';
if (typedKey === 'status') {
formattedValue =
value === 'NOT_SEND'
? 'ارسال نشده'
: value === 'SUCCESS'
? 'موفق'
: value === 'FAILURE'
? 'ناموفق'
: value === 'QUEUED'
? 'در انتظار ارسال'
: String(value);
} else if (
key === 'invoice_date_from' ||
key === 'invoice_date_to' ||
key === 'created_at_from' ||
key === 'created_at_to'
) {
formattedValue = gregorianToJalali(value);
} else {
formattedValue = String(value);
}
return { key: typedKey, label: labels[typedKey] ?? typedKey, value: formattedValue };
});
});
@@ -74,6 +88,7 @@ export class PosSaleInvoiceListComponent {
backRoute = POS_ROUTES.path || '/';
ngOnInit() {
this.hydrateFiltersFromQueryParams();
this.getData();
}
@@ -96,6 +111,7 @@ export class PosSaleInvoiceListComponent {
onFilterApply(query: IPosSaleInvoicesFilterDto) {
this.filters.set(query);
this.syncFiltersToQueryParams();
this.getData();
}
@@ -103,10 +119,57 @@ export class PosSaleInvoiceListComponent {
const next = { ...this.filters() };
delete next[key];
this.filters.set(next);
this.syncFiltersToQueryParams();
this.getData();
}
toSinglePage(item: IPosSaleInvoicesResponse) {
this.router.navigateByUrl(posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(item.id));
}
private hydrateFiltersFromQueryParams() {
const queryParams = this.route.snapshot.queryParams;
const keys: (keyof IPosSaleInvoicesFilterDto)[] = [
'invoice_date_from',
'invoice_date_to',
'created_at_from',
'created_at_to',
'code',
'customer_name',
'customer_mobile',
'customer_national_id',
'customer_economic_code',
'status',
'total_amount_from',
'total_amount_to',
];
const parsed = keys.reduce<IPosSaleInvoicesFilterDto>((acc, key) => {
const value = queryParams[key];
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {});
this.filters.set(parsed);
}
private syncFiltersToQueryParams() {
const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>(
(acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = String(value);
}
return acc;
},
{}
);
this.router.navigate([], {
relativeTo: this.route,
queryParams,
replaceUrl: true,
});
}
}
@@ -10,7 +10,7 @@ export interface IPosSaleInvoicesFilterDto {
customer_mobile?: string;
customer_national_id?: string;
customer_economic_code?: string;
status?: string;
status?: 'NOT_SEND' | 'SUCCESS' | 'FAILURE' | 'QUEUED';
total_amount?: number | null;
total_amount_from?: number | null;
total_amount_to?: number | null;
@@ -0,0 +1,20 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
import config from 'src/config';
export type TShopRouteNames = 'shop';
const baseRoute = `${config.isPosApplication ? '' : '/pos'}/shop`;
export const PosShopNamedRoutes: NamedRoutes<TShopRouteNames> = {
shop: {
path: 'shop',
loadComponent: () => import('../../views/root.component').then((m) => m.PosShopComponent),
meta: {
title: 'فروشگاه',
pagePath: () => baseRoute,
},
},
};
export const POS_SHOP_ROUTES: Routes = [PosShopNamedRoutes.shop];
@@ -14,7 +14,7 @@ import { IPosOrderResponse } from '../models';
import { PosLandingStore } from '../store/main.store';
@Component({
selector: 'pos-landing',
selector: 'pos-shop',
templateUrl: './root.component.html',
host: { class: 'grow overflow-hidden' },
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -29,7 +29,7 @@ import { PosLandingStore } from '../store/main.store';
PosOrderSubmittedDialogComponent,
],
})
export class PosLandingComponent extends AbstractIsMobileComponent {
export class PosShopComponent extends AbstractIsMobileComponent {
private readonly infoStore = inject(PosInfoStore);
private readonly landingStore = inject(PosLandingStore);
@@ -0,0 +1 @@
export * from './invoice-type-card.component';
@@ -0,0 +1,33 @@
<div
class="text-surface-700 relative flex flex-col gap-4 overflow-hidden rounded-2xl border px-4 py-4 pe-2"
[style.backgroundColor]="preparedInfo().bgColor"
[style.borderColor]="preparedInfo().borderColor"
(click)="showDetails()">
<div class="absolute top-0 right-0 h-full w-1" [style.backgroundColor]="preparedInfo().borderColor"></div>
<div class="text-surface-700 flex items-center justify-between gap-2">
<div class="text-surface-700 flex items-center gap-2">
<span
class="text-surface-700 flex h-8 w-8 items-center justify-center rounded-full border"
[style.borderColor]="preparedInfo().borderColor"
[style.backgroundColor]="preparedInfo().borderColor + '33'">
<i class="text-sm" [ngClass]="'pi pi-' + preparedInfo().icon" [style.color]="preparedInfo().borderColor"></i>
</span>
<span class="text-surface-700 text-sm font-semibold">{{ preparedInfo().title }}</span>
</div>
<span
class="text-surface-700 rounded-full border px-2 py-1 text-xs font-bold"
[style.borderColor]="preparedInfo().borderColor">
{{ count || 0 }} عدد
</span>
</div>
<div class="text-surface-700 grid grid-cols-1 gap-2">
<div class="flex items-center gap-2">
<div class="text-surface-700/80 text-xs">مبلغ کل:</div>
<div class="text-surface-700 mt-1 text-sm font-bold">{{ formattedTotalAmount }}</div>
</div>
<div class="flex items-center gap-2">
<div class="text-surface-700/80 text-xs">مالیات کل:</div>
<div class="text-surface-700 mt-1 text-sm font-bold">{{ formattedTotalTax }}</div>
</div>
</div>
</div>
@@ -0,0 +1,77 @@
import { formatWithCurrency } from '@/utils';
import { CommonModule } from '@angular/common';
import { Component, computed, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'pos-statistics-invoice-type-card',
templateUrl: 'invoice-type-card.component.html',
imports: [CommonModule],
})
export class PosStatisticsInvoiceTypeCardComponent {
@Input() type: 'all' | 'success' | 'failure' | 'notSended' | 'pending' | 'credit' = 'all';
@Input() count?: number = 0;
@Input() totalAmount?: number = 0;
@Input() totalTaxAmount?: number = 0;
@Input() loading = false;
@Input() invoicesRoute = '';
@Output() onClick = new EventEmitter<void>();
get formattedTotalAmount() {
return formatWithCurrency(this.totalAmount || 0);
}
get formattedTotalTax() {
return formatWithCurrency(this.totalTaxAmount || 0);
}
readonly preparedInfo = computed(() => {
switch (this.type) {
case 'all':
return {
title: 'همه',
icon: 'close',
bgColor: '#6B72801A',
borderColor: '#6B7280',
};
case 'success':
return {
title: 'تایید شده',
icon: 'check',
bgColor: '#22C55E1A',
borderColor: '#22C55E',
};
case 'failure':
return {
title: 'خطادار',
icon: 'times',
bgColor: '#EF44441A',
borderColor: '#EF4444',
};
case 'notSended':
return {
title: 'ارسال نشده',
icon: 'clock',
bgColor: '#F59E0B1A',
borderColor: '#F59E0B',
};
case 'pending':
return {
title: 'انتظار ارسال',
icon: 'clock',
bgColor: '#3B82F61A',
borderColor: '#3B82F6',
};
case 'credit':
return {
title: 'اعتبار',
icon: 'credit-card',
bgColor: '#8B5CF61A',
borderColor: '#8B5CF6',
};
}
});
showDetails() {
this.onClick.emit();
}
}
@@ -0,0 +1,5 @@
const baseUrl = () => `/api/v1/pos/statistics`;
export const POS_STATISTICS_API_ROUTES = {
getAll: () => `${baseUrl()}/sale-invoices`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes/index';
export * from './routes/index';
@@ -0,0 +1,21 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
import config from 'src/config';
export type TStatisticsRouteNames = 'statistics';
const baseRoute = `${config.isPosApplication ? '' : '/pos'}`;
export const StatisticsRoutes: NamedRoutes<TStatisticsRouteNames> = {
statistics: {
path: '',
loadComponent: () =>
import('../../views/root.component').then((m) => m.PosStatisticsRootComponent),
meta: {
title: 'داشبودر',
pagePath: () => baseRoute,
},
},
};
export const POS_STATISTICS_ROUTES: Routes = [StatisticsRoutes.statistics];
@@ -0,0 +1 @@
export * from './io';
+15
View File
@@ -0,0 +1,15 @@
export interface IPosStatisticsRawResponse {
all: IPosStatisticsItem;
success: IPosStatisticsItem;
failure: IPosStatisticsItem;
pending: IPosStatisticsItem;
notSended: IPosStatisticsItem;
credit: IPosStatisticsItem;
}
export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {}
interface IPosStatisticsItem {
count: number;
total_amount: number;
total_tax: number;
}
@@ -0,0 +1,18 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { POS_STATISTICS_API_ROUTES } from '../constants';
import { IPosStatisticsRawResponse, IPosStatisticsResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class PosStatisticsService {
constructor(private http: HttpClient) {}
private apiRoutes = POS_STATISTICS_API_ROUTES;
getAll(fromDate?: Date): Observable<IPosStatisticsResponse> {
return this.http.get<IPosStatisticsRawResponse>(this.apiRoutes.getAll(), {
params: { from_date: fromDate?.toISOString() || '' },
});
}
}
@@ -0,0 +1,50 @@
import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state';
import { inject, Injectable } from '@angular/core';
import { catchError, finalize } from 'rxjs';
import { IPosStatisticsResponse } from '../models';
import { PosStatisticsService } from '../services/main.service';
interface PosStatisticsState extends EntityState<IPosStatisticsResponse> {
lastFromDate: Date;
}
@Injectable({
providedIn: 'root',
})
export class PosStatisticsStore extends EntityStore<IPosStatisticsResponse, PosStatisticsState> {
private readonly service = inject(PosStatisticsService);
constructor() {
super({
...defaultBaseStateData,
lastFromDate: new Date(),
});
}
getData(fromDate?: Date) {
if (fromDate && fromDate.getTime() === this._state().lastFromDate.getTime()) {
return;
}
this.patchState({ loading: true, lastFromDate: fromDate || new Date() });
this.service
.getAll(fromDate)
.pipe(
finalize(() => {
this.patchState({ loading: false });
}),
catchError((error) => {
this.setError(error);
throw error;
})
)
.subscribe((entity) => {
this.patchState({ entity });
});
}
override reset(): void {
this.setState({
...defaultBaseStateData,
lastFromDate: new Date(),
});
}
}
@@ -0,0 +1 @@
export * from './root.component';
@@ -0,0 +1,57 @@
<div class="flex flex-col gap-6 p-4">
<season-picker (onChange)="onDateChanged($event)" />
<div class="grid grid-cols-2 gap-4">
@if (loading()) {
@for (_ of [0, 1, 2, 3, 4, 5]; track $index) {
<div class="h-32 w-full">
<p-skeleton width="100%" height="100%"></p-skeleton>
</div>
}
} @else {
<pos-statistics-invoice-type-card
type="all"
[loading]="loading()"
[totalAmount]="item()?.all?.total_amount"
[totalTaxAmount]="item()?.all?.total_tax"
[count]="item()?.all?.count"
(onClick)="toInvoicesPage('all')" />
<pos-statistics-invoice-type-card
type="success"
[loading]="loading()"
[totalAmount]="item()?.success?.total_amount"
[totalTaxAmount]="item()?.success?.total_tax"
[count]="item()?.success?.count"
(onClick)="toInvoicesPage('success')" />
<pos-statistics-invoice-type-card
type="failure"
[loading]="loading()"
[totalAmount]="item()?.failure?.total_amount"
[totalTaxAmount]="item()?.failure?.total_tax"
[count]="item()?.failure?.count"
(onClick)="toInvoicesPage('failure')" />
<pos-statistics-invoice-type-card
type="notSended"
[loading]="loading()"
[totalAmount]="item()?.notSended?.total_amount"
[totalTaxAmount]="item()?.notSended?.total_tax"
[count]="item()?.notSended?.count"
(onClick)="toInvoicesPage('notSended')" />
<pos-statistics-invoice-type-card
type="pending"
[loading]="loading()"
[totalAmount]="item()?.pending?.total_amount"
[totalTaxAmount]="item()?.pending?.total_tax"
[count]="item()?.pending?.count"
(onClick)="toInvoicesPage('pending')" />
<pos-statistics-invoice-type-card
type="credit"
[loading]="loading()"
[totalAmount]="item()?.credit?.total_amount"
[totalTaxAmount]="item()?.credit?.total_tax"
[count]="item()?.credit?.count" />
}
</div>
<div class="fixed inset-x-0 bottom-0 p-4">
<button pButton [routerLink]="shopRoute" size="large" class="w-full">ایجاد صورت‌حساب جدید</button>
</div>
</div>
@@ -0,0 +1,76 @@
import { SeasonPickerComponent } from '@/shared/components/seasonPicker/season-picker.component';
import { formatGregorian, gregorianToJalali, parseJalali } from '@/utils';
import { Component, computed, inject, signal } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { Skeleton } from 'primeng/skeleton';
import { posSaleInvoicesNamedRoutes } from '../../saleInvoices/constants';
import { IPosSaleInvoicesFilterDto } from '../../saleInvoices/models';
import { PosShopNamedRoutes } from '../../shop/constants/routes';
import { PosStatisticsInvoiceTypeCardComponent } from '../components';
import { PosStatisticsStore } from '../store/main.store';
@Component({
selector: 'pos-statistics-root',
templateUrl: './root.component.html',
imports: [
PosStatisticsInvoiceTypeCardComponent,
Skeleton,
SeasonPickerComponent,
ButtonDirective,
RouterLink,
],
})
export class PosStatisticsRootComponent {
private readonly store = inject(PosStatisticsStore);
private readonly router = inject(Router);
readonly item = computed(() => this.store.entity());
readonly loading = computed(() => this.store.loading());
readonly shopRoute = PosShopNamedRoutes.shop.meta.pagePath!();
selectedDate = signal(new Date());
ngOnInit() {
this.getData(this.selectedDate());
}
onDateChanged(date: Date) {
this.selectedDate.set(date);
this.getData(date);
}
getData(date?: Date) {
this.store.getData(date);
}
toInvoicesPage(type: string) {
const mainRoute = posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!();
const queryParams: IPosSaleInvoicesFilterDto = {
invoice_date_from: this.selectedDate().toISOString(),
invoice_date_to: new Date(
formatGregorian(
parseJalali(gregorianToJalali(this.selectedDate())).add(3, 'months').set('day', 1)
)
).toISOString(),
};
switch (type) {
case 'success':
queryParams.status = 'SUCCESS';
break;
case 'failure':
queryParams.status = 'FAILURE';
break;
case 'notSended':
queryParams.status = 'NOT_SEND';
break;
case 'pending':
queryParams.status = 'QUEUED';
break;
}
const queryString = new URLSearchParams(queryParams as Record<string, string>).toString();
this.router.navigateByUrl(`${mainRoute}?${queryString}`);
}
}
+4 -5
View File
@@ -3,21 +3,20 @@ import { POS_ABOUT_ROUTES } from './modules/about/constants';
import { POS_CONFIG_ROUTES } from './modules/configs/constants';
import { POS_GOODS_MANAGEMENT_ROUTES } from './modules/goodsManagement/constants';
import { POS_SALE_INVOICES_ROUTES } from './modules/saleInvoices/constants';
import { POS_SHOP_ROUTES } from './modules/shop/constants/routes';
import { POS_STATISTICS_ROUTES } from './modules/statistics/constants';
import { POS_SUPPORT_ROUTES } from './modules/support/constants';
export const POS_ROUTES = {
path: '',
loadComponent: () => import('@/layout/default/app.layout.component').then((m) => m.AppLayout),
children: [
{
path: '',
loadComponent: () =>
import('./modules/landing/views/root.component').then((m) => m.PosLandingComponent),
},
...POS_SHOP_ROUTES,
...POS_SALE_INVOICES_ROUTES,
...POS_ABOUT_ROUTES,
...POS_SUPPORT_ROUTES,
...POS_CONFIG_ROUTES,
...POS_GOODS_MANAGEMENT_ROUTES,
...POS_STATISTICS_ROUTES,
],
} as Route;
+2 -2
View File
@@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, finalize, map } from 'rxjs';
import { IPosProfileResponse } from '../models';
import { PosService } from '../modules/landing/services/main.service';
import { PosService } from '../modules/shop/services/main.service';
interface PosProfileState extends EntityState<IPosProfileResponse> {}
@@ -37,7 +37,7 @@ export class PosProfileStore extends EntityStore<IPosProfileResponse, PosProfile
this.setEntity(entity);
}
return entity;
}),
})
);
}
+2 -2
View File
@@ -5,7 +5,7 @@ import { CookieService } from 'ngx-cookie-service';
import { catchError, finalize, map } from 'rxjs';
import { COOKIE_KEYS } from 'src/assets/constants';
import { IPosInfoResponse } from '../models/pos.io';
import { PosService } from '../modules/landing/services/main.service';
import { PosService } from '../modules/shop/services/main.service';
interface PosState extends EntityState<IPosInfoResponse> {
posId: string;
@@ -44,7 +44,7 @@ export class PosInfoStore extends EntityStore<IPosInfoResponse, PosState> {
this.setEntity(entity);
}
return entity;
}),
})
);
}