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:
@@ -209,6 +209,8 @@
|
|||||||
".webp": "file"
|
".webp": "file"
|
||||||
},
|
},
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"node_modules/flatpickr-wrap/dist/flatpickr.css",
|
||||||
|
"node_modules/flatpickr-wrap/dist/themes/confetti.css",
|
||||||
"src/assets/styles.scss"
|
"src/assets/styles.scss"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.app.json"
|
"tsConfig": "tsconfig.app.json"
|
||||||
@@ -250,6 +252,8 @@
|
|||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"node_modules/flatpickr-wrap/dist/flatpickr.css",
|
||||||
|
"node_modules/flatpickr-wrap/dist/themes/confetti.css",
|
||||||
"src/assets/styles.scss"
|
"src/assets/styles.scss"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json"
|
"tsConfig": "tsconfig.spec.json"
|
||||||
|
|||||||
Generated
+3
-3
@@ -3682,7 +3682,7 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
esbuild@0.28.0:
|
esbuild@0.28.0:
|
||||||
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==, tarball: https://hub.megan.ir/repository/npm/esbuild/-/esbuild-0.28.0.tgz}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -4122,7 +4122,7 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://hub.megan.ir/repository/npm/graceful-fs/-/graceful-fs-4.2.11.tgz}
|
||||||
|
|
||||||
handle-thing@2.0.1:
|
handle-thing@2.0.1:
|
||||||
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
|
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
|
||||||
@@ -6058,7 +6058,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1:
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, tarball: https://hub.megan.ir/repository/npm/source-map/-/source-map-0.6.1.tgz}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
source-map@0.7.6:
|
source-map@0.7.6:
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import ISummary from '@/core/models/summary';
|
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 {
|
export interface ISalesInvoicesRawResponse {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { RouterLink } from '@angular/router';
|
|||||||
import { Button } from 'primeng/button';
|
import { Button } from 'primeng/button';
|
||||||
import { Drawer } from 'primeng/drawer';
|
import { Drawer } from 'primeng/drawer';
|
||||||
import { Ripple } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import config from 'src/config';
|
|
||||||
import { posAboutNamedRoutes } from '../../modules/about/constants';
|
import { posAboutNamedRoutes } from '../../modules/about/constants';
|
||||||
import { posConfigNamedRoutes } from '../../modules/configs/constants';
|
import { posConfigNamedRoutes } from '../../modules/configs/constants';
|
||||||
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
|
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
|
||||||
import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/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 { posSupportNamedRoutes } from '../../modules/support/constants';
|
||||||
import { PosInfoStore } from '../../store';
|
import { PosInfoStore } from '../../store';
|
||||||
|
|
||||||
@@ -37,10 +38,15 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
|
|||||||
|
|
||||||
readonly menuItems = [
|
readonly menuItems = [
|
||||||
{
|
{
|
||||||
label: 'فروش',
|
label: 'صفحه اصلی',
|
||||||
routerLink: config.isPosApplication ? '/' : '/pos',
|
routerLink: StatisticsRoutes.statistics.meta.pagePath!(),
|
||||||
icon: 'pi pi-fw pi-home',
|
icon: 'pi pi-fw pi-home',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'فروش',
|
||||||
|
routerLink: PosShopNamedRoutes.shop.meta.pagePath!(),
|
||||||
|
icon: 'pi pi-fw pi-cart-arrow-down',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'فاکتورها',
|
label: 'فاکتورها',
|
||||||
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
|
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Message } from 'primeng/message';
|
|||||||
import { catchError, finalize } from 'rxjs';
|
import { catchError, finalize } from 'rxjs';
|
||||||
import { COOKIE_KEYS } from 'src/assets/constants';
|
import { COOKIE_KEYS } from 'src/assets/constants';
|
||||||
import { IPosAccessibleResponse } from '../../models/pos.io';
|
import { IPosAccessibleResponse } from '../../models/pos.io';
|
||||||
import { PosService } from '../../modules/landing/services/main.service';
|
import { PosService } from '../../modules/shop/services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-choose-cards',
|
selector: 'pos-choose-cards',
|
||||||
@@ -35,7 +35,7 @@ export class PosChooseCardsComponent {
|
|||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.error.set(err);
|
this.error.set(err);
|
||||||
throw err;
|
throw err;
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.subscribe((res) => {
|
.subscribe((res) => {
|
||||||
this.items.set(res?.data || []);
|
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">
|
<app-card-data cardTitle="تنظیمات فاکتور" class="w-full">
|
||||||
<p-message variant="text" severity="info" icon="pi pi-info-circle">
|
<p-message variant="text" severity="info" icon="pi pi-info-circle">
|
||||||
هریک از موارد زیر را که میخواهید در چاپ فاکتور نمایش داده شوند را انتخاب کنید
|
هریک از موارد زیر را که میخواهید در چاپ فاکتور نمایش داده شوند را انتخاب کنید
|
||||||
</p-message>
|
</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>
|
</app-card-data>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppCardComponent } from '@/shared/components';
|
import { AppCardComponent } from '@/shared/components';
|
||||||
|
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Message } from 'primeng/message';
|
import { Message } from 'primeng/message';
|
||||||
@@ -7,7 +8,7 @@ import { PosConfigPrintFormComponent } from '../components/print/form.component'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-config-page',
|
selector: 'pos-config-page',
|
||||||
templateUrl: './root.component.html',
|
templateUrl: './root.component.html',
|
||||||
imports: [PosConfigPrintFormComponent, AppCardComponent, Message],
|
imports: [PosConfigPrintFormComponent, AppCardComponent, Message, UikitFlatpickrJalaliComponent],
|
||||||
})
|
})
|
||||||
export class PosConfigPageComponent {
|
export class PosConfigPageComponent {
|
||||||
private readonly router = inject(Router);
|
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_mobile: this.fb.control<string>(''),
|
||||||
customer_national_id: this.fb.control<string>(''),
|
customer_national_id: this.fb.control<string>(''),
|
||||||
customer_economic_code: 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_from: this.fb.control<number | null>(null),
|
||||||
total_amount_to: 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_from: this.value.total_amount_from ?? null,
|
||||||
total_amount_to: this.value.total_amount_to ?? 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 { POS_ROUTES } from '@/domains/pos/routes';
|
||||||
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
||||||
|
import { gregorianToJalali } from '@/utils';
|
||||||
import { Component, computed, inject, signal } from '@angular/core';
|
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 { Button, ButtonDirective } from 'primeng/button';
|
||||||
import { Chip } from 'primeng/chip';
|
import { Chip } from 'primeng/chip';
|
||||||
import { Skeleton } from 'primeng/skeleton';
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
@@ -32,6 +33,7 @@ import { SaleInvoiceCardComponent } from './sale-invoice-card.component';
|
|||||||
export class PosSaleInvoiceListComponent {
|
export class PosSaleInvoiceListComponent {
|
||||||
private readonly service = inject(PosSaleInvoicesService);
|
private readonly service = inject(PosSaleInvoicesService);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
items = signal<IPosSaleInvoicesSummaryResponse[]>([]);
|
items = signal<IPosSaleInvoicesSummaryResponse[]>([]);
|
||||||
@@ -57,16 +59,28 @@ export class PosSaleInvoiceListComponent {
|
|||||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const typedKey = key as keyof IPosSaleInvoicesFilterDto;
|
const typedKey = key as keyof IPosSaleInvoicesFilterDto;
|
||||||
const formattedValue =
|
let formattedValue = '';
|
||||||
typedKey === 'status' && value === 'NOT_SEND'
|
if (typedKey === 'status') {
|
||||||
|
formattedValue =
|
||||||
|
value === 'NOT_SEND'
|
||||||
? 'ارسال نشده'
|
? 'ارسال نشده'
|
||||||
: typedKey === 'status' && value === 'SUCCESS'
|
: value === 'SUCCESS'
|
||||||
? 'موفق'
|
? 'موفق'
|
||||||
: typedKey === 'status' && value === 'FAILURE'
|
: value === 'FAILURE'
|
||||||
? 'ناموفق'
|
? 'ناموفق'
|
||||||
: typedKey === 'status' && value === 'QUEUED'
|
: value === 'QUEUED'
|
||||||
? 'در انتظار ارسال'
|
? 'در انتظار ارسال'
|
||||||
: String(value);
|
: 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 };
|
return { key: typedKey, label: labels[typedKey] ?? typedKey, value: formattedValue };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -74,6 +88,7 @@ export class PosSaleInvoiceListComponent {
|
|||||||
backRoute = POS_ROUTES.path || '/';
|
backRoute = POS_ROUTES.path || '/';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.hydrateFiltersFromQueryParams();
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +111,7 @@ export class PosSaleInvoiceListComponent {
|
|||||||
|
|
||||||
onFilterApply(query: IPosSaleInvoicesFilterDto) {
|
onFilterApply(query: IPosSaleInvoicesFilterDto) {
|
||||||
this.filters.set(query);
|
this.filters.set(query);
|
||||||
|
this.syncFiltersToQueryParams();
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +119,57 @@ export class PosSaleInvoiceListComponent {
|
|||||||
const next = { ...this.filters() };
|
const next = { ...this.filters() };
|
||||||
delete next[key];
|
delete next[key];
|
||||||
this.filters.set(next);
|
this.filters.set(next);
|
||||||
|
this.syncFiltersToQueryParams();
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
toSinglePage(item: IPosSaleInvoicesResponse) {
|
toSinglePage(item: IPosSaleInvoicesResponse) {
|
||||||
this.router.navigateByUrl(posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(item.id));
|
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_mobile?: string;
|
||||||
customer_national_id?: string;
|
customer_national_id?: string;
|
||||||
customer_economic_code?: string;
|
customer_economic_code?: string;
|
||||||
status?: string;
|
status?: 'NOT_SEND' | 'SUCCESS' | 'FAILURE' | 'QUEUED';
|
||||||
total_amount?: number | null;
|
total_amount?: number | null;
|
||||||
total_amount_from?: number | null;
|
total_amount_from?: number | null;
|
||||||
total_amount_to?: 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];
|
||||||
+2
-2
@@ -14,7 +14,7 @@ import { IPosOrderResponse } from '../models';
|
|||||||
import { PosLandingStore } from '../store/main.store';
|
import { PosLandingStore } from '../store/main.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-landing',
|
selector: 'pos-shop',
|
||||||
templateUrl: './root.component.html',
|
templateUrl: './root.component.html',
|
||||||
host: { class: 'grow overflow-hidden' },
|
host: { class: 'grow overflow-hidden' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -29,7 +29,7 @@ import { PosLandingStore } from '../store/main.store';
|
|||||||
PosOrderSubmittedDialogComponent,
|
PosOrderSubmittedDialogComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PosLandingComponent extends AbstractIsMobileComponent {
|
export class PosShopComponent extends AbstractIsMobileComponent {
|
||||||
private readonly infoStore = inject(PosInfoStore);
|
private readonly infoStore = inject(PosInfoStore);
|
||||||
private readonly landingStore = inject(PosLandingStore);
|
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';
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,21 +3,20 @@ import { POS_ABOUT_ROUTES } from './modules/about/constants';
|
|||||||
import { POS_CONFIG_ROUTES } from './modules/configs/constants';
|
import { POS_CONFIG_ROUTES } from './modules/configs/constants';
|
||||||
import { POS_GOODS_MANAGEMENT_ROUTES } from './modules/goodsManagement/constants';
|
import { POS_GOODS_MANAGEMENT_ROUTES } from './modules/goodsManagement/constants';
|
||||||
import { POS_SALE_INVOICES_ROUTES } from './modules/saleInvoices/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';
|
import { POS_SUPPORT_ROUTES } from './modules/support/constants';
|
||||||
|
|
||||||
export const POS_ROUTES = {
|
export const POS_ROUTES = {
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('@/layout/default/app.layout.component').then((m) => m.AppLayout),
|
loadComponent: () => import('@/layout/default/app.layout.component').then((m) => m.AppLayout),
|
||||||
children: [
|
children: [
|
||||||
{
|
...POS_SHOP_ROUTES,
|
||||||
path: '',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./modules/landing/views/root.component').then((m) => m.PosLandingComponent),
|
|
||||||
},
|
|
||||||
...POS_SALE_INVOICES_ROUTES,
|
...POS_SALE_INVOICES_ROUTES,
|
||||||
...POS_ABOUT_ROUTES,
|
...POS_ABOUT_ROUTES,
|
||||||
...POS_SUPPORT_ROUTES,
|
...POS_SUPPORT_ROUTES,
|
||||||
...POS_CONFIG_ROUTES,
|
...POS_CONFIG_ROUTES,
|
||||||
...POS_GOODS_MANAGEMENT_ROUTES,
|
...POS_GOODS_MANAGEMENT_ROUTES,
|
||||||
|
...POS_STATISTICS_ROUTES,
|
||||||
],
|
],
|
||||||
} as Route;
|
} as Route;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { catchError, finalize, map } from 'rxjs';
|
import { catchError, finalize, map } from 'rxjs';
|
||||||
import { IPosProfileResponse } from '../models';
|
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> {}
|
interface PosProfileState extends EntityState<IPosProfileResponse> {}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export class PosProfileStore extends EntityStore<IPosProfileResponse, PosProfile
|
|||||||
this.setEntity(entity);
|
this.setEntity(entity);
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CookieService } from 'ngx-cookie-service';
|
|||||||
import { catchError, finalize, map } from 'rxjs';
|
import { catchError, finalize, map } from 'rxjs';
|
||||||
import { COOKIE_KEYS } from 'src/assets/constants';
|
import { COOKIE_KEYS } from 'src/assets/constants';
|
||||||
import { IPosInfoResponse } from '../models/pos.io';
|
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> {
|
interface PosState extends EntityState<IPosInfoResponse> {
|
||||||
posId: string;
|
posId: string;
|
||||||
@@ -44,7 +44,7 @@ export class PosInfoStore extends EntityStore<IPosInfoResponse, PosState> {
|
|||||||
this.setEntity(entity);
|
this.setEntity(entity);
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export class PublicSaleInvoiceStore extends EntityStore<
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe((entity) => {
|
.subscribe((entity) => {
|
||||||
console.log('entity', entity);
|
|
||||||
|
|
||||||
this.patchState({ entity });
|
this.patchState({ entity });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<shared-light-bottomsheet [(visible)]="visible">
|
||||||
|
<div class="mt-1">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-slate-600">انتخاب سال</label>
|
||||||
|
<select
|
||||||
|
[(ngModel)]="selectedYearDraft"
|
||||||
|
class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-700 outline-none">
|
||||||
|
@for (yearItem of years; track yearItem.value) {
|
||||||
|
<option [value]="yearItem.value">{{ yearItem.year }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||||
|
@for (seasonItem of seasons; track seasonItem.key) {
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
[label]="seasonItem.label"
|
||||||
|
[severity]="seasonItem.severity"
|
||||||
|
outlined
|
||||||
|
(click)="selectedSeasonDraft.set(seasonItem.key)"></button>
|
||||||
|
}
|
||||||
|
<!-- [disabled]="selectedYear <= selectedYearDraft() && selectedSeason <= selectedSeasonDraft()" -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<app-form-footer-actions
|
||||||
|
submitLabel="تایید"
|
||||||
|
cancelLabel="انصراف"
|
||||||
|
(onCancel)="visibleChange.emit(false)"
|
||||||
|
(onSubmit)="onSubmit()" />
|
||||||
|
</div>
|
||||||
|
</shared-light-bottomsheet>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ButtonDirective, ButtonSeverity } from 'primeng/button';
|
||||||
|
import { SharedLightBottomsheetComponent } from '../dialog/light-bottomsheet.component';
|
||||||
|
import { FormFooterActionsComponent } from '../formFooterActions/form-footer-actions.component';
|
||||||
|
|
||||||
|
type SeasonKey = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
interface SeasonItem {
|
||||||
|
key: SeasonKey;
|
||||||
|
label: string;
|
||||||
|
severity: ButtonSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'season-picker-dialog',
|
||||||
|
templateUrl: './season-picker-dialog.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
SharedLightBottomsheetComponent,
|
||||||
|
FormFooterActionsComponent,
|
||||||
|
ButtonDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SeasonPickerDialogComponent extends AbstractDialog {
|
||||||
|
@Input() years: { year: number; value: number }[] = [];
|
||||||
|
@Input() selectedYear!: number;
|
||||||
|
@Input() selectedSeason!: SeasonKey;
|
||||||
|
@Input() seasons: SeasonItem[] = [];
|
||||||
|
|
||||||
|
@Output() submit = new EventEmitter<{ year: number; season: number }>();
|
||||||
|
|
||||||
|
selectedYearDraft = signal(this.selectedYear);
|
||||||
|
selectedSeasonDraft = signal(this.selectedSeason);
|
||||||
|
|
||||||
|
isSeasonDisabled = computed(() => this.selectedYear >= this.selectedYearDraft());
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.submit.emit({
|
||||||
|
year: this.selectedYearDraft(),
|
||||||
|
season: this.selectedSeasonDraft(),
|
||||||
|
});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.selectedYearDraft.set(this.selectedYear);
|
||||||
|
this.selectedSeasonDraft.set(this.selectedSeason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="flex items-center gap-2 overflow-hidden rounded-xl border border-slate-200 bg-white p-2">
|
||||||
|
<p-button icon="pi pi-chevron-right" size="small" text class="shrink-0" (onClick)="prevSeason()" />
|
||||||
|
<div class="flex flex-1 cursor-pointer justify-center" (click)="openPicker()">
|
||||||
|
<span class="text-base font-semibold">{{ currentSeason() }}</span>
|
||||||
|
</div>
|
||||||
|
<p-button icon="pi pi-chevron-left" size="small" text class="shrink-0" (onClick)="nextSeason()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isOpen()) {
|
||||||
|
<season-picker-dialog
|
||||||
|
[visible]="isOpen()"
|
||||||
|
[years]="years()"
|
||||||
|
[seasons]="seasons"
|
||||||
|
[selectedYear]="selectedYear()"
|
||||||
|
[selectedSeason]="selectedSeason()"
|
||||||
|
(visibleChange)="isOpen.set($event)"
|
||||||
|
(submit)="submitPicker($event)" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { gregorianToJalali, JALALI_DATE_FORMATS, jalaliToGregorian, nowJalali } from '@/utils';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Input, OnInit, Output, signal } from '@angular/core';
|
||||||
|
import { Button, ButtonSeverity } from 'primeng/button';
|
||||||
|
import { SeasonPickerDialogComponent } from './season-picker-dialog.component';
|
||||||
|
|
||||||
|
type SeasonKey = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
interface SeasonItem {
|
||||||
|
key: SeasonKey;
|
||||||
|
label: string;
|
||||||
|
severity: ButtonSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'season-picker',
|
||||||
|
templateUrl: 'season-picker.component.html',
|
||||||
|
imports: [CommonModule, Button, SeasonPickerDialogComponent],
|
||||||
|
})
|
||||||
|
export class SeasonPickerComponent implements OnInit {
|
||||||
|
@Input() minYear = 1390;
|
||||||
|
@Input() maxYear = Number(nowJalali(JALALI_DATE_FORMATS.YEAR));
|
||||||
|
@Input() value!: Date;
|
||||||
|
|
||||||
|
@Output() onChange = new EventEmitter<Date>();
|
||||||
|
|
||||||
|
readonly isOpen = signal(false);
|
||||||
|
readonly selectedYear = signal(Number(nowJalali(JALALI_DATE_FORMATS.YEAR)));
|
||||||
|
readonly selectedSeason = signal<SeasonKey>(0);
|
||||||
|
|
||||||
|
readonly seasons: SeasonItem[] = [
|
||||||
|
{ key: 0, label: 'بهار', severity: 'success' },
|
||||||
|
{ key: 1, label: 'تابستان', severity: 'danger' },
|
||||||
|
{ key: 2, label: 'پاییز', severity: 'warn' },
|
||||||
|
{ key: 3, label: 'زمستان', severity: 'info' },
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly years = signal<{ year: number; value: number }[]>([]);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const year = Number(gregorianToJalali(this.value, JALALI_DATE_FORMATS.YEAR));
|
||||||
|
const monthIndex = Number(gregorianToJalali(this.value, JALALI_DATE_FORMATS.MONTH)) - 1;
|
||||||
|
const season = Math.floor(monthIndex / 3) as SeasonKey;
|
||||||
|
|
||||||
|
this.selectedYear.set(year);
|
||||||
|
this.selectedSeason.set(season);
|
||||||
|
this.years.set(
|
||||||
|
Array.from({ length: this.maxYear - this.minYear + 1 }, (_, i) => ({
|
||||||
|
year: this.minYear + i,
|
||||||
|
value: this.minYear + i,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
this.emitValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSeason(): string {
|
||||||
|
const season = this.seasons.find((item) => item.key === this.selectedSeason());
|
||||||
|
return `${season?.label ?? ''} ${this.selectedYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
openPicker(): void {
|
||||||
|
this.isOpen.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePicker(): void {
|
||||||
|
this.isOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSeason(): void {
|
||||||
|
const index = this.selectedSeason();
|
||||||
|
const currentYear = this.selectedYear();
|
||||||
|
const nextSeason = index === 0 ? 3 : ((index - 1) as SeasonKey);
|
||||||
|
const nextYear = index === 0 ? currentYear - 1 : currentYear;
|
||||||
|
|
||||||
|
// if (!this.isWithinBounds(nextYear, nextSeason)) return;
|
||||||
|
|
||||||
|
this.selectedYear.set(nextYear);
|
||||||
|
this.selectedSeason.set(nextSeason);
|
||||||
|
this.emitValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSeason(): void {
|
||||||
|
const index = this.selectedSeason();
|
||||||
|
const currentYear = this.selectedYear();
|
||||||
|
const nextSeason = index === 3 ? 0 : ((index + 1) as SeasonKey);
|
||||||
|
const nextYear = index === 3 ? currentYear + 1 : currentYear;
|
||||||
|
|
||||||
|
this.selectedYear.set(nextYear);
|
||||||
|
this.selectedSeason.set(nextSeason);
|
||||||
|
this.emitValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPicker(value: { year: number; season: number }): void {
|
||||||
|
this.selectedYear.set(value.year);
|
||||||
|
this.selectedSeason.set(value.season as SeasonKey);
|
||||||
|
this.emitValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWithinBounds(year: number, season: SeasonKey): boolean {
|
||||||
|
if (year < this.minYear || year > this.maxYear) return false;
|
||||||
|
// if (year === this.minYear && season < this.minSeason) return false;
|
||||||
|
// if (year === this.maxYear && season > this.maxSeason) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitValue(): void {
|
||||||
|
this.onChange.emit(
|
||||||
|
new Date(
|
||||||
|
jalaliToGregorian(
|
||||||
|
`${this.selectedYear()}/${(this.selectedSeason() * 3 + 1).toString().padStart(2, '0')}/01`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,7 @@
|
|||||||
<uikit-field
|
<div #wrapper [attr.data-name]="name" class="flatWrapper w-full">
|
||||||
[label]="label"
|
<input data-input [attr.placeholder]="placeholder" [name]="name" class="flatpicker-field w-full" />
|
||||||
[control]="control"
|
|
||||||
[showLabel]="showLabel"
|
|
||||||
[showErrors]="showErrors"
|
|
||||||
class="w-full datepicker"
|
|
||||||
>
|
|
||||||
<div class="w-full">
|
|
||||||
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full">
|
|
||||||
<input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
|
|
||||||
|
|
||||||
@if (hint) {
|
@if (hint) {
|
||||||
<small [attr.id]="name + '-help'">{{ hint }}</small>
|
<small [attr.id]="name + '-help'">{{ hint }}</small>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flat"></div>
|
|
||||||
</div>
|
|
||||||
</uikit-field>
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { jalaliToGregorian, nowJalali } from '@/utils';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnInit,
|
OnDestroy,
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import flatpickr from 'flatpickr-wrap';
|
import flatpickr from 'flatpickr-wrap';
|
||||||
|
import { Persian } from 'flatpickr-wrap/dist/l10n/fa';
|
||||||
import { BaseOptions, DateOption } from 'flatpickr-wrap/dist/types/options';
|
import { BaseOptions, DateOption } from 'flatpickr-wrap/dist/types/options';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { UikitFieldComponent } from '../uikit-field.component';
|
import { UikitFieldComponent } from '../uikit-field.component';
|
||||||
@@ -22,7 +25,7 @@ import { UikitFieldComponent } from '../uikit-field.component';
|
|||||||
imports: [CommonModule, ReactiveFormsModule, InputTextModule, UikitFieldComponent],
|
imports: [CommonModule, ReactiveFormsModule, InputTextModule, UikitFieldComponent],
|
||||||
templateUrl: './datepicker.component.html',
|
templateUrl: './datepicker.component.html',
|
||||||
})
|
})
|
||||||
export class UikitFlatpickrJalaliComponent implements OnInit {
|
export class UikitFlatpickrJalaliComponent implements AfterViewInit, OnDestroy {
|
||||||
@Input() control?: FormControl<Maybe<string>>;
|
@Input() control?: FormControl<Maybe<string>>;
|
||||||
@Input() placeholder = 'انتخاب تاریخ';
|
@Input() placeholder = 'انتخاب تاریخ';
|
||||||
@Input() label = 'تاریخ';
|
@Input() label = 'تاریخ';
|
||||||
@@ -41,19 +44,34 @@ export class UikitFlatpickrJalaliComponent implements OnInit {
|
|||||||
|
|
||||||
private fpInstance: any | null = null;
|
private fpInstance: any | null = null;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
|
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
|
||||||
|
wrap: true,
|
||||||
...this.options,
|
...this.options,
|
||||||
locale: 'fa',
|
locale: Persian,
|
||||||
|
disableMobile: true,
|
||||||
|
clickOpens: !this.disabled,
|
||||||
minDate: this.minDate,
|
minDate: this.minDate,
|
||||||
maxDate: this.maxDate,
|
maxDate: this.maxDate,
|
||||||
altInputClass: 'p-inputtext w-full',
|
now: nowJalali(),
|
||||||
|
altInputClass: 'flatpicker-field w-full',
|
||||||
|
altInput: true,
|
||||||
dateFormat: 'Y-m-d',
|
dateFormat: 'Y-m-d',
|
||||||
altFormat: 'Y-m-d',
|
altFormat: 'Y/m/d',
|
||||||
|
|
||||||
onChange: (selectedDates: Date[], dateStr: string) => {
|
onChange: (selectedDates: Date[], dateStr: string) => {
|
||||||
|
console.log('dateStr', jalaliToGregorian(dateStr));
|
||||||
|
|
||||||
this.valueChange.emit(dateStr);
|
this.valueChange.emit(dateStr);
|
||||||
if (this.control) this.control.setValue(dayjs(dateStr).toISOString());
|
if (this.control) this.control.setValue(dayjs(dateStr).toISOString());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.fpInstance?.destroy) {
|
||||||
|
this.fpInstance.destroy();
|
||||||
|
this.fpInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export const JALALI_DATE_FORMATS = {
|
|||||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||||
/** Month and year: "شهریور ۱۳۹۷" */
|
/** Month and year: "شهریور ۱۳۹۷" */
|
||||||
MONTH_YEAR: 'MMMM YYYY',
|
MONTH_YEAR: 'MMMM YYYY',
|
||||||
|
/** Year: "۱۳۹۷" */
|
||||||
|
YEAR: 'YYYY',
|
||||||
|
/** Month: "۰۶" */
|
||||||
|
MONTH: 'MM',
|
||||||
/** Day and month: "۱۳ شهریور" */
|
/** Day and month: "۱۳ شهریور" */
|
||||||
DAY_MONTH: 'DD MMMM',
|
DAY_MONTH: 'DD MMMM',
|
||||||
} as const;
|
} as const;
|
||||||
@@ -55,6 +59,10 @@ export const GREGORIAN_DATE_FORMATS = {
|
|||||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||||
/** Month and year: "September 2018" */
|
/** Month and year: "September 2018" */
|
||||||
MONTH_YEAR: 'MMMM YYYY',
|
MONTH_YEAR: 'MMMM YYYY',
|
||||||
|
/** Year: "2018" */
|
||||||
|
YEAR: 'YYYY',
|
||||||
|
/** Month: "06" */
|
||||||
|
MONTH: 'MM',
|
||||||
/** Day and month: "September 04" */
|
/** Day and month: "September 04" */
|
||||||
DAY_MONTH: 'MMMM DD',
|
DAY_MONTH: 'MMMM DD',
|
||||||
} as const;
|
} as const;
|
||||||
@@ -73,7 +81,7 @@ export const GREGORIAN_DATE_FORMATS = {
|
|||||||
*/
|
*/
|
||||||
export function formatJalali(
|
export function formatJalali(
|
||||||
date: string | Date | Dayjs | number,
|
date: string | Date | Dayjs | number,
|
||||||
format: string = JALALI_DATE_FORMATS.NUMERIC,
|
format: string = JALALI_DATE_FORMATS.NUMERIC
|
||||||
): string {
|
): string {
|
||||||
return dayjs(date).calendar('jalali').locale('fa').format(format);
|
return dayjs(date).calendar('jalali').locale('fa').format(format);
|
||||||
}
|
}
|
||||||
@@ -91,7 +99,7 @@ export function formatJalali(
|
|||||||
*/
|
*/
|
||||||
export function formatGregorian(
|
export function formatGregorian(
|
||||||
date: string | Date | Dayjs | number,
|
date: string | Date | Dayjs | number,
|
||||||
format: string = GREGORIAN_DATE_FORMATS.NUMERIC,
|
format: string = GREGORIAN_DATE_FORMATS.NUMERIC
|
||||||
): string {
|
): string {
|
||||||
return dayjs(date).calendar('gregory').locale('en').format(format);
|
return dayjs(date).calendar('gregory').locale('en').format(format);
|
||||||
}
|
}
|
||||||
@@ -107,7 +115,7 @@ export function formatGregorian(
|
|||||||
* parseJalali('1398-10-17') // Dayjs instance
|
* parseJalali('1398-10-17') // Dayjs instance
|
||||||
* parseJalali('1398/10/17') // Dayjs instance
|
* parseJalali('1398/10/17') // Dayjs instance
|
||||||
*/
|
*/
|
||||||
export function parseJalali(jalaliDate: string, format?: string): Dayjs {
|
export function parseJalali(jalaliDate: string | Date | Dayjs | number, format?: string): Dayjs {
|
||||||
return dayjs(jalaliDate, { jalali: true, ...(format && { format }) });
|
return dayjs(jalaliDate, { jalali: true, ...(format && { format }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +132,7 @@ export function parseJalali(jalaliDate: string, format?: string): Dayjs {
|
|||||||
*/
|
*/
|
||||||
export function gregorianToJalali(
|
export function gregorianToJalali(
|
||||||
gregorianDate: string | Date | Dayjs,
|
gregorianDate: string | Date | Dayjs,
|
||||||
format: string = JALALI_DATE_FORMATS.NUMERIC,
|
format: string = JALALI_DATE_FORMATS.NUMERIC
|
||||||
): string {
|
): string {
|
||||||
return dayjs(gregorianDate).calendar('jalali').locale('fa').format(format);
|
return dayjs(gregorianDate).calendar('jalali').locale('fa').format(format);
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,7 @@ export function gregorianToJalali(
|
|||||||
*/
|
*/
|
||||||
export function jalaliToGregorian(
|
export function jalaliToGregorian(
|
||||||
jalaliDate: string,
|
jalaliDate: string,
|
||||||
format: string = GREGORIAN_DATE_FORMATS.NUMERIC,
|
format: string = GREGORIAN_DATE_FORMATS.NUMERIC
|
||||||
): string {
|
): string {
|
||||||
return parseJalali(jalaliDate).calendar('gregory').locale('en').format(format);
|
return parseJalali(jalaliDate).calendar('gregory').locale('en').format(format);
|
||||||
}
|
}
|
||||||
@@ -245,7 +253,7 @@ export function formatDate(
|
|||||||
date: string | Date | Dayjs | number,
|
date: string | Date | Dayjs | number,
|
||||||
calendar: 'jalali' | 'gregory',
|
calendar: 'jalali' | 'gregory',
|
||||||
locale: 'fa' | 'en',
|
locale: 'fa' | 'en',
|
||||||
format: string,
|
format: string
|
||||||
): string {
|
): string {
|
||||||
return dayjs(date).calendar(calendar).locale(locale).format(format);
|
return dayjs(date).calendar(calendar).locale(locale).format(format);
|
||||||
}
|
}
|
||||||
@@ -296,21 +304,21 @@ export function getJalaliWeekdayName(dayNumber: number): string {
|
|||||||
export function jalaliDiff(
|
export function jalaliDiff(
|
||||||
date1: string | number | Date | Dayjs,
|
date1: string | number | Date | Dayjs,
|
||||||
date2: string | number | Date | Dayjs,
|
date2: string | number | Date | Dayjs,
|
||||||
unit: dayjs.OpUnitType = 'day',
|
unit: dayjs.OpUnitType = 'day'
|
||||||
): number {
|
): number {
|
||||||
return toJalali(date1).diff(toJalali(date2), unit);
|
return toJalali(date1).diff(toJalali(date2), unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isJalaliBefore(
|
export function isJalaliBefore(
|
||||||
date1: string | number | Date | Dayjs,
|
date1: string | number | Date | Dayjs,
|
||||||
date2: string | number | Date | Dayjs,
|
date2: string | number | Date | Dayjs
|
||||||
): boolean {
|
): boolean {
|
||||||
return toJalali(date1).isBefore(toJalali(date2));
|
return toJalali(date1).isBefore(toJalali(date2));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isJalaliAfter(
|
export function isJalaliAfter(
|
||||||
date1: string | number | Date | Dayjs,
|
date1: string | number | Date | Dayjs,
|
||||||
date2: string | number | Date | Dayjs,
|
date2: string | number | Date | Dayjs
|
||||||
): boolean {
|
): boolean {
|
||||||
return toJalali(date1).isAfter(toJalali(date2));
|
return toJalali(date1).isAfter(toJalali(date2));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@use "./tailwind.css";
|
@use "./tailwind.css";
|
||||||
// @use "flatpickr-wrap/dist/themes/confetti.css";
|
@use "flatpickr-wrap/dist/themes/confetti.css";
|
||||||
@use "flatpickr-wrap/dist/flatpickr.css";
|
@use "flatpickr-wrap/dist/flatpickr.css";
|
||||||
@use "./flatpicker.css";
|
@use "./flatpicker.css";
|
||||||
@use "./layout/layout.scss";
|
@use "./layout/layout.scss";
|
||||||
|
|||||||
+14
-16
@@ -1,25 +1,23 @@
|
|||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import flatpickr from 'flatpickr-wrap';
|
|
||||||
import faLocales from 'flatpickr-wrap/dist/l10n/fa.js';
|
|
||||||
import { brandingConfig } from './app/branding/branding.config';
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { appConfig } from './app.config';
|
import { appConfig } from './app.config';
|
||||||
|
import { brandingConfig } from './app/branding/branding.config';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
flatpickr.localize(faLocales.fa!);
|
// flatpickr.localize(faLocales.fa!);
|
||||||
|
|
||||||
// Apply default configuration requested by the project
|
// // Apply default configuration requested by the project
|
||||||
if (flatpickr.defaultConfig) {
|
// if (flatpickr.defaultConfig) {
|
||||||
Object.assign(flatpickr.defaultConfig, {
|
// Object.assign(flatpickr.defaultConfig, {
|
||||||
altInput: true,
|
// altInput: true,
|
||||||
altFormat: 'F j, Y H:i',
|
// altFormat: 'F j, Y H:i',
|
||||||
dateFormat: 'Y-m-d',
|
// dateFormat: 'Y-m-d',
|
||||||
time_24hr: true,
|
// time_24hr: true,
|
||||||
time_format: 'H:i',
|
// time_format: 'H:i',
|
||||||
wrap: true,
|
// wrap: true,
|
||||||
clickOpens: true,
|
// clickOpens: true,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
document.title = brandingConfig.appTitle;
|
document.title = brandingConfig.appTitle;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user