Compare commits

...

5 Commits

101 changed files with 1366 additions and 13696 deletions
+1
View File
@@ -43,3 +43,4 @@ testem.log
Thumbs.db Thumbs.db
.env.* .env.*
.env
+78 -19
View File
@@ -13,25 +13,6 @@ Stack:
--- ---
# EXECUTION RULES
- Keep responses short.
- Do not narrate thoughts.
- Do not explain obvious steps.
- Do not create plans for simple tasks.
- Prefer implementation over exploration.
Avoid:
- “I think…”
- “Let me check…”
- “I should inspect…”
- “Im going to…”
Prefer:
- “Updated environment files.”
- “Applied tenant config fix.”
---
# RTK RULES (MANDATORY) # RTK RULES (MANDATORY)
@@ -192,3 +173,81 @@ Updated:
Validation: Validation:
- tsc passed - tsc passed
# TARGETED READ RULES
Do not read files larger than 300 lines unless required.
For changes at a known location:
- read the surrounding symbol only
- avoid full-file reads
Prefer:
- rtk grep
- rtk smart
- targeted read
Avoid:
- opening 400+ line files for small edits
# REASONING OUTPUT RULES
Do not expose internal reasoning.
Never output:
- "I think..."
- "I'm considering..."
- "I wonder..."
- "Maybe..."
- implementation deliberation
Never explain alternative approaches unless requested.
Use:
Inspecting confirmation dialog.
Updating async accept support.
Validation passed.
# INVESTIGATION LIMITS
For localized fixes:
- maximum 3 file reads before first edit
- maximum 1 related-file read unless required
Stop searching once the edit target is identified.
# REVIEW MODE
During reviews:
- inspect changed files only
- avoid loading unrelated dependencies
- avoid architecture exploration
- avoid repository-wide searches
Review only code directly affected by the diff.
# HARD TOKEN LIMITS
For localized fixes:
- Never read files larger than 300 lines unless the target symbol cannot be isolated.
- Never read an entire file when a symbol-level read is possible.
- Never read more than 5 total files before the first edit.
- Never open a file already summarized by `rtk smart` unless implementation details are required.
When a file exceeds 300 lines:
1. rtk grep
2. rtk smart
3. read only the relevant symbol/section
Avoid full-file reads.
+1 -6
View File
@@ -190,8 +190,7 @@
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"dayjs", "dayjs",
"dayjs/locale/fa", "dayjs/locale/fa",
"dayjs/plugin/relativeTime", "dayjs/plugin/relativeTime"
"flatpickr-wrap/dist/l10n/fa.js"
], ],
"assets": [ "assets": [
{ {
@@ -209,8 +208,6 @@
".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"
@@ -252,8 +249,6 @@
"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"
-12993
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -12,12 +12,9 @@
"@primeng/themes": "^20.4.0", "@primeng/themes": "^20.4.0",
"@primeuix/themes": "^1.2.5", "@primeuix/themes": "^1.2.5",
"@tailwindcss/postcss": "^4.2.3", "@tailwindcss/postcss": "^4.2.3",
"@zoomit/dayjs-jalali-plugin": "^0.1.11",
"angularx-qrcode": "20.0.0", "angularx-qrcode": "20.0.0",
"chart.js": "4.4.2", "chart.js": "4.4.2",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"flatpickr": "^4.6.13",
"flatpickr-wrap": "^1.0.0",
"jalaliday": "^3.1.1", "jalaliday": "^3.1.1",
"jest-editor-support": "32.0.0-beta.1", "jest-editor-support": "32.0.0-beta.1",
"ngx-cookie-service": "^21.3.1", "ngx-cookie-service": "^21.3.1",
+9 -3
View File
@@ -12,7 +12,7 @@ import {
withInterceptors, withInterceptors,
withInterceptorsFromDi, withInterceptorsFromDi,
} from '@angular/common/http'; } from '@angular/common/http';
import { ApplicationConfig, isDevMode, provideZonelessChangeDetection } from '@angular/core'; import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { import {
provideRouter, provideRouter,
@@ -34,7 +34,7 @@ export const appConfig: ApplicationConfig = {
anchorScrolling: 'enabled', anchorScrolling: 'enabled',
scrollPositionRestoration: 'enabled', scrollPositionRestoration: 'enabled',
}), }),
withEnabledBlockingInitialNavigation(), withEnabledBlockingInitialNavigation()
), ),
provideZonelessChangeDetection(), provideZonelessChangeDetection(),
// configure HttpClient once: enable fetch and register both functional // configure HttpClient once: enable fetch and register both functional
@@ -42,6 +42,12 @@ export const appConfig: ApplicationConfig = {
provideAnimationsAsync(), provideAnimationsAsync(),
// ensure PrimeNG uses our preset (applies css variables at app initialization) // ensure PrimeNG uses our preset (applies css variables at app initialization)
providePrimeNG({ providePrimeNG({
zIndex: {
tooltip: 2100,
menu: 2000,
overlay: 2000,
modal: 1100,
},
theme: { theme: {
preset: MyPreset, preset: MyPreset,
options: { darkModeSelector: '.app-dark' }, options: { darkModeSelector: '.app-dark' },
@@ -72,7 +78,7 @@ export const appConfig: ApplicationConfig = {
provideHttpClient( provideHttpClient(
withFetch(), withFetch(),
withInterceptors([loggingInterceptor, authInterceptor, errorInterceptor, dedupInterceptor]), withInterceptors([loggingInterceptor, authInterceptor, errorInterceptor, dedupInterceptor]),
withInterceptorsFromDi(), withInterceptorsFromDi()
), ),
// provideServiceWorker('ngsw-worker.js', { // provideServiceWorker('ngsw-worker.js', {
// enabled: !isDevMode(), // enabled: !isDevMode(),
@@ -11,8 +11,6 @@ export function greaterThanValidator(minValue: number): ValidatorFn {
return null; return null;
} }
console.log('v', v);
const numericValue = typeof v === 'number' ? v : Number(v); const numericValue = typeof v === 'number' ? v : Number(v);
if (Number.isNaN(numericValue)) { if (Number.isNaN(numericValue)) {
return { greaterThan: 'مقدار باید عددی باشد' }; return { greaterThan: 'مقدار باید عددی باشد' };
@@ -18,7 +18,7 @@ export const CONSUMER_MENU_ITEMS = [
routerLink: ['/consumer/poses'], routerLink: ['/consumer/poses'],
}, },
{ {
label: 'فاکتورها', label: 'صورت‌حساب‌ها',
icon: 'pi pi-fw pi-receipt', icon: 'pi pi-fw pi-receipt',
routerLink: ['/consumer/sale_invoices'], routerLink: ['/consumer/sale_invoices'],
}, },
@@ -1,9 +1,8 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست فاکتور‌ها" pageTitle="لیست صورت‌حساب‌ها"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به حال فاکتور فروشی توسط این پایانه ایجاد نشده است." emptyPlaceholderTitle="تا به حال صورت‌حسابی توسط این پایانه ایجاد نشده است."
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
(onRefresh)="refresh()" (onRefresh)="refresh()">
>
</app-page-data-list> </app-page-data-list>
@@ -1,10 +1,9 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'لیست فاکتورها'" [pageTitle]="'لیست صورت‌حساب‌'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="فاکتوری یافت نشد" emptyPlaceholderTitle="صورت‌حسابی یافت نشد"
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
[showDetails]="true" [showDetails]="true"
(onDetails)="toSinglePage($event)" (onDetails)="toSinglePage($event)"
(onRefresh)="refresh()" (onRefresh)="refresh()" />
/>
@@ -40,7 +40,7 @@ export class ConsumerCustomerSaleInvoiceListComponent extends AbstractList<ICust
}, },
{ {
field: 'invoice_date', field: 'invoice_date',
header: 'تاریخ فاکتور', header: 'تاریخ صورت‌حساب‌',
type: 'date', type: 'date',
}, },
{ {
@@ -63,7 +63,7 @@ export class ConsumerCustomerSaleInvoiceListComponent extends AbstractList<ICust
toSinglePage(item: ICustomerSaleInvoicesResponse) { toSinglePage(item: ICustomerSaleInvoicesResponse) {
this.router.navigateByUrl( this.router.navigateByUrl(
consumerCustomerSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.customerId, item.id), consumerCustomerSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.customerId, item.id)
); );
} }
} }
@@ -9,10 +9,10 @@ export const consumerCustomerSaleInvoicesNamedRoutes: NamedRoutes<TConsumerCusto
path: 'saleInvoices', path: 'saleInvoices',
loadComponent: () => loadComponent: () =>
import('../../views/saleInvoices/list.component').then( import('../../views/saleInvoices/list.component').then(
(m) => m.ConsumerCustomerSaleInvoicesComponent, (m) => m.ConsumerCustomerSaleInvoicesComponent
), ),
meta: { meta: {
title: 'فاکتورهای صادر شده', title: 'صورت‌حساب‌های صادر شده',
pagePath: (customerId: string) => `consumer/customers/${customerId}`, pagePath: (customerId: string) => `consumer/customers/${customerId}`,
}, },
}, },
@@ -20,10 +20,10 @@ export const consumerCustomerSaleInvoicesNamedRoutes: NamedRoutes<TConsumerCusto
path: 'saleInvoices/:invoiceId', path: 'saleInvoices/:invoiceId',
loadComponent: () => loadComponent: () =>
import('../../views/saleInvoices/single.component').then( import('../../views/saleInvoices/single.component').then(
(m) => m.ConsumerCustomerSaleInvoiceComponent, (m) => m.ConsumerCustomerSaleInvoiceComponent
), ),
meta: { meta: {
title: 'فاکتور صادر شده', title: 'صورت‌حساب‌ صادر شده',
pagePath: (customerId: string, saleInvoiceId: string) => pagePath: (customerId: string, saleInvoiceId: string) =>
`consumer/customers/${customerId}/saleInvoices/${saleInvoiceId}`, `consumer/customers/${customerId}/saleInvoices/${saleInvoiceId}`,
}, },
@@ -1,15 +1,14 @@
<app-page-data-list <app-page-data-list
pageTitle="آخرین فاکتورهای صادر شده امروز" pageTitle="آخرین صورت‌حساب‌های صادر شده امروز"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به این لحظه فاکتوری صادر نشده است." emptyPlaceholderTitle="تا به این لحظه صورت‌حساب‌ی صادر نشده است."
[items]="items()" [items]="items()"
[showDetails]="true" [showDetails]="true"
[loading]="loading()" [loading]="loading()"
(onDetails)="toSinglePage($event)" (onDetails)="toSinglePage($event)"
(onRefresh)="refresh()" (onRefresh)="refresh()">
>
<ng-template #moreActions> <ng-template #moreActions>
<a routerLink pButton [routerLink]="invoicesPageRoute" outlined>تمامی فاکتورها</a> <a routerLink pButton [routerLink]="invoicesPageRoute" outlined>تمامی صورت‌حساب‌ها</a>
</ng-template> </ng-template>
<ng-template #status let-item> <ng-template #status let-item>
@@ -45,7 +45,7 @@ export class ConsumerStatisticsLatestInvoicesComponent extends AbstractList<ISta
}, },
{ {
field: 'invoice_date', field: 'invoice_date',
header: 'تاریخ فاکتور', header: 'تاریخ صورت‌حساب‌',
type: 'dateTime', type: 'dateTime',
}, },
{ field: 'status', header: 'وضعیت صدور', customDataModel: this.status }, { field: 'status', header: 'وضعیت صدور', customDataModel: this.status },
@@ -58,7 +58,7 @@ export class ConsumerStatisticsLatestInvoicesComponent extends AbstractList<ISta
toSinglePage(invoice: IStatisticsSaleInvoicesResponse) { toSinglePage(invoice: IStatisticsSaleInvoicesResponse) {
this.router.navigateByUrl( this.router.navigateByUrl(
consumerSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(invoice.id), consumerSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(invoice.id)
); );
} }
} }
@@ -1,7 +1,7 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'لیست فاکتورها'" [pageTitle]="'لیست صورت‌حساب‌ها'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="فاکتوری یافت نشد" emptyPlaceholderTitle="صورت‌حساب‌ی یافت نشد"
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
[perPage]="responseMetaData()?.perPage" [perPage]="responseMetaData()?.perPage"
@@ -9,7 +9,7 @@ export const consumerSaleInvoicesNamedRoutes: NamedRoutes<TConsumerSaleInvoicesR
loadComponent: () => loadComponent: () =>
import('../../views/list.component').then((m) => m.ConsumerSaleInvoicesComponent), import('../../views/list.component').then((m) => m.ConsumerSaleInvoicesComponent),
meta: { meta: {
title: 'فاکتورهای صادر شده', title: 'صورت‌حساب‌های صادر شده',
pagePath: () => `/consumer/sale_invoices`, pagePath: () => `/consumer/sale_invoices`,
}, },
}, },
@@ -18,7 +18,7 @@ export const consumerSaleInvoicesNamedRoutes: NamedRoutes<TConsumerSaleInvoicesR
loadComponent: () => loadComponent: () =>
import('../../views/single.component').then((m) => m.ConsumerSaleInvoiceComponent), import('../../views/single.component').then((m) => m.ConsumerSaleInvoiceComponent),
meta: { meta: {
title: 'فاکتور صادر شده', title: 'صورت‌حساب‌ صادر شده',
pagePath: (saleInvoiceId: string) => `/consumer/sale_invoices/${saleInvoiceId}`, pagePath: (saleInvoiceId: string) => `/consumer/sale_invoices/${saleInvoiceId}`,
}, },
}, },
@@ -4,15 +4,14 @@
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ width: '500px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<uikit-datepicker <!-- <uikit-datepicker
label="تاریخ شروع لایسنس" label="تاریخ شروع لایسنس"
[control]="form.controls.starts_at" [control]="form.controls.starts_at"
name="starts_at" name="starts_at"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/> /> -->
<!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> --> <!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> -->
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
@@ -1,7 +1,7 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component'; import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; // import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, computed, inject, Input } from '@angular/core'; import { Component, computed, inject, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { ILicenseRequest, ILicenseResponse } from '../../models'; import { ILicenseRequest, ILicenseResponse } from '../../models';
@@ -10,12 +10,7 @@ import { LicensesService } from '../../services/licenses.service';
@Component({ @Component({
selector: 'partner-consumer-license-form', selector: 'partner-consumer-license-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
imports: [ imports: [ReactiveFormsModule, SharedDialogComponent, FormFooterActionsComponent],
ReactiveFormsModule,
SharedDialogComponent,
FormFooterActionsComponent,
UikitFlatpickrJalaliComponent,
],
}) })
export class ConsumerLicenseFormComponent extends AbstractFormDialog< export class ConsumerLicenseFormComponent extends AbstractFormDialog<
ILicenseRequest, ILicenseRequest,
@@ -8,7 +8,7 @@ export const CONSUMER_MENU_ITEMS = [
routerLink: ['/'], routerLink: ['/'],
}, },
{ {
label: 'فاکتورها', label: 'صورت‌حساب‌ها',
icon: 'pi pi-fw pi-home', icon: 'pi pi-fw pi-home',
}, },
], ],
@@ -13,7 +13,7 @@
<pos-change-password-form class="w-full" /> <pos-change-password-form class="w-full" />
<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="mt-6 block w-full" (onClose)="returnToMainPage()" /> <pos-config-print-form class="mt-6 block w-full" (onClose)="returnToMainPage()" />
</app-card-data> </app-card-data>
@@ -4,12 +4,12 @@
[modal]="true" [modal]="true"
[dismissible]="true" [dismissible]="true"
[showCloseIcon]="true" [showCloseIcon]="true"
header="فیلتر فاکتورها" header="فیلتر صورت‌حساب‌ها"
styleClass="w-full md:w-[28rem]" styleClass="w-full md:w-[28rem]"
(visibleChange)="visibleChange.emit($event)"> (visibleChange)="visibleChange.emit($event)">
<div class="flex h-full flex-col overflow-y-auto"> <div class="flex h-full flex-col">
<hr /> <hr />
<form class="grow py-4" [formGroup]="form" (ngSubmit)="submit()"> <form class="grow py-0" [formGroup]="form" (ngSubmit)="submit()">
<app-input label="شماره صورت‌حساب" [control]="form.controls.invoice_number" name="invoice_number" /> <app-input label="شماره صورت‌حساب" [control]="form.controls.invoice_number" name="invoice_number" />
<app-enum-select type="fiscalResponseStatus" [control]="form.controls.status" /> <app-enum-select type="fiscalResponseStatus" [control]="form.controls.status" />
@@ -23,12 +23,26 @@
type="nationalId" /> type="nationalId" />
<app-input label="شناسه اقتصادی" [control]="form.controls.customer_economic_code" name="customer_economic_code" /> <app-input label="شناسه اقتصادی" [control]="form.controls.customer_economic_code" name="customer_economic_code" />
<!-- <div class="grid grid-cols-2 gap-2"> <app-datepicker
<uikit-datepicker label="از تاریخ فاکتور" [control]="form.controls.invoice_date_from" name="invoice_date_from" /> label="از تاریخ صورت‌حساب‌"
<uikit-datepicker label="تا تاریخ فاکتور" [control]="form.controls.invoice_date_to" name="invoice_date_to" /> [control]="form.controls.invoice_date_from"
</div> [max]="form.controls.invoice_date_to.value || now"
name="invoice_date_from" />
<app-datepicker
label="تا تاریخ صورت‌حساب‌"
[control]="form.controls.invoice_date_to"
[min]="form.controls.invoice_date_from.value || undefined"
[max]="now"
name="invoice_date_to" />
<div class="grid grid-cols-2 gap-2"> <app-datepicker
label="از تاریخ ایجاد"
[control]="form.controls.created_at_from"
[max]="form.controls.invoice_date_to.value || now"
name="created_at_from" />
<app-datepicker label="تا تاریخ ایجاد" [control]="form.controls.created_at_to" [max]="now" name="created_at_to" />
<!-- <div class="grid grid-cols-2 gap-2">
<uikit-datepicker label="از تاریخ صدور" [control]="form.controls.created_at_from" name="created_at_from" /> <uikit-datepicker label="از تاریخ صدور" [control]="form.controls.created_at_from" name="created_at_from" />
<uikit-datepicker label="تا تاریخ صدور" [control]="form.controls.created_at_to" name="created_at_to" /> <uikit-datepicker label="تا تاریخ صدور" [control]="form.controls.created_at_to" name="created_at_to" />
</div> --> </div> -->
@@ -38,7 +52,7 @@
<app-input label="تا مبلغ" [control]="form.controls.total_amount_to" name="total_amount_to" type="price" /> <app-input label="تا مبلغ" [control]="form.controls.total_amount_to" name="total_amount_to" type="price" />
</div> </div>
</form> </form>
<div class="bg-surface-card sticky bottom-0 mt-auto shrink-0"> <div class="bg-surface-card mt-auto shrink-0 pb-4">
<hr /> <hr />
<div class="grid grid-cols-2 gap-2 pt-2"> <div class="grid grid-cols-2 gap-2 pt-2">
<button pButton type="button" outlined label="حذف" (click)="clear()"></button> <button pButton type="button" outlined label="حذف" (click)="clear()"></button>
@@ -2,6 +2,8 @@ import { Maybe } from '@/core';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component'; import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { TspProviderResponseStatus } from '@/shared/catalog'; import { TspProviderResponseStatus } from '@/shared/catalog';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { DatepickerComponent } from '@/uikit';
import { nowGregorian } from '@/utils';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -29,6 +31,7 @@ import { IPosSaleInvoicesFilterDto } from '../models';
ButtonDirective, ButtonDirective,
EnumSelectComponent, EnumSelectComponent,
InputComponent, InputComponent,
DatepickerComponent,
], ],
}) })
export class PosSaleInvoicesFilterDrawerComponent implements OnChanges { export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
@@ -40,6 +43,8 @@ export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
@Output() visibleChange = new EventEmitter<boolean>(); @Output() visibleChange = new EventEmitter<boolean>();
@Output() apply = new EventEmitter<IPosSaleInvoicesFilterDto>(); @Output() apply = new EventEmitter<IPosSaleInvoicesFilterDto>();
readonly now = nowGregorian();
readonly form = this.fb.group({ readonly form = this.fb.group({
page: this.fb.control<number | null>(null), page: this.fb.control<number | null>(null),
perPage: this.fb.control<number | null>(null), perPage: this.fb.control<number | null>(null),
@@ -1,5 +1,5 @@
<div class="bg-surface-card"> <div class="bg-surface-card">
<app-inner-pages-header pageTitle="لیست فاکتورهای صادر شده" [backRoute]="backRoute"> <app-inner-pages-header pageTitle="لیست صورت‌حساب‌های صادر شده" [backRoute]="backRoute">
<ng-template #actions> <ng-template #actions>
<div class="flex gap-1"> <div class="flex gap-1">
<p-button <p-button
@@ -34,7 +34,7 @@
} }
} @else if (!items() || items().length === 0) { } @else if (!items() || items().length === 0) {
<div class="col-span-1"> <div class="col-span-1">
<p class="text-muted-color text-center">فاکتوری یافت نشد.</p> <p class="text-muted-color text-center">صورت‌حساب‌ی یافت نشد.</p>
</div> </div>
} @else { } @else {
@for (item of items(); track item.id) { @for (item of items(); track item.id) {
@@ -48,8 +48,8 @@ export class PosSaleInvoiceListComponent {
filters = signal<IPosSaleInvoicesFilterDto>({}); filters = signal<IPosSaleInvoicesFilterDto>({});
activeFilters = computed(() => { activeFilters = computed(() => {
const labels: Partial<Record<keyof IPosSaleInvoicesFilterDto, string>> = { const labels: Partial<Record<keyof IPosSaleInvoicesFilterDto, string>> = {
invoice_date_from: 'از تاریخ فاکتور', invoice_date_from: 'از تاریخ صورت‌حساب‌',
invoice_date_to: 'تا تاریخ فاکتور', invoice_date_to: 'تا تاریخ صورت‌حساب‌',
created_at_from: 'از تاریخ ایجاد', created_at_from: 'از تاریخ ایجاد',
created_at_to: 'تا تاریخ ایجاد', created_at_to: 'تا تاریخ ایجاد',
invoice_number: 'شماره صورت‌حساب', invoice_number: 'شماره صورت‌حساب',
@@ -160,6 +160,7 @@ export class PosSaleInvoiceListComponent {
} }
private syncFiltersToQueryParams() { private syncFiltersToQueryParams() {
this.filters.update((val) => ({ ...val, page: 1 }));
const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>( const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>(
(acc, [key, value]) => { (acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') { if (value !== undefined && value !== null && value !== '') {
@@ -9,10 +9,10 @@
{{ preparedInvoiceType() }} {{ preparedInvoiceType() }}
</app-key-value> </app-key-value>
<app-key-value alignment="end" label="مبلغ کل" [value]="saleInvoice.total_amount" type="price" /> <app-key-value alignment="end" label="مبلغ کل" [value]="saleInvoice.total_amount" type="price" />
<app-key-value alignment="end" label="نوع فاکتور"> <app-key-value alignment="end" label="نوع صورت‌حساب‌">
<catalog-invoice-type-tag [status]="saleInvoice.type.value" /> <catalog-invoice-type-tag [status]="saleInvoice.type.value" />
</app-key-value> </app-key-value>
<app-key-value alignment="end" label="تاریخ فاکتور" [value]="saleInvoice.created_at" type="dateTime" /> <app-key-value alignment="end" label="تاریخ صورت‌حساب‌" [value]="saleInvoice.created_at" type="date" />
</div> </div>
<hr /> <hr />
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
@@ -27,7 +27,7 @@
} }
@if (saleInvoice.status.value.toLowerCase() === 'not_send') { @if (saleInvoice.status.value.toLowerCase() === 'not_send') {
<p-button <p-button
label="ارسال فاکتور" label="ارسال صورت‌حساب‌"
type="button" type="button"
(click)="sendInvoice()" (click)="sendInvoice()"
[loading]="sendingLoading()" [loading]="sendingLoading()"
@@ -74,12 +74,12 @@ export class SaleInvoiceCardComponent {
.subscribe((res) => { .subscribe((res) => {
if (res.status === 'SEND_FAILURE') { if (res.status === 'SEND_FAILURE') {
this.toastService.error({ this.toastService.error({
text: res.message || 'خطا در ارسال فاکتور رخ داده است.', text: res.message || 'خطا در ارسال صورت‌حساب‌ رخ داده است.',
}); });
} else { } else {
this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ارسال صورت‌حساب‌ با موفقیت انجام شد.' });
} }
// this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' }); // this.toastService.success({ text: 'ارسال صورت‌حساب‌ با موفقیت انجام شد.' });
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
} }
@@ -91,7 +91,7 @@ export class SaleInvoiceCardComponent {
.pipe(finalize(() => this.gettingStatusLoading.set(false))) .pipe(finalize(() => this.gettingStatusLoading.set(false)))
.subscribe((res) => { .subscribe((res) => {
this.toastService.info({ this.toastService.info({
text: `وضعیت فعلی فاکتور شماره‌ی ${this.saleInvoice.invoice_number}، ${taxProviderStatusTranslatorUtil(res.status)} می‌باشد.`, text: `وضعیت فعلی صورت‌حساب‌ شماره‌ی ${this.saleInvoice.invoice_number}، ${taxProviderStatusTranslatorUtil(res.status)} می‌باشد.`,
}); });
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
@@ -104,9 +104,11 @@ export class SaleInvoiceCardComponent {
.pipe(finalize(() => this.resendingLoading.set(false))) .pipe(finalize(() => this.resendingLoading.set(false)))
.subscribe((res) => { .subscribe((res) => {
if (res.status === 'FAILURE') { if (res.status === 'FAILURE') {
this.toastService.error({ text: res.message || 'خطا در ارسال مجدد فاکتور رخ داده است.' }); this.toastService.error({
text: res.message || 'خطا در ارسال مجدد صورت‌حساب‌ رخ داده است.',
});
} else { } else {
this.toastService.success({ text: 'ارسال مجدد فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ارسال مجدد صورت‌حساب‌ با موفقیت انجام شد.' });
} }
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
@@ -118,7 +120,7 @@ export class SaleInvoiceCardComponent {
.revoke(this.saleInvoice.id) .revoke(this.saleInvoice.id)
.pipe(finalize(() => this.revokingInvoiceLoading.set(false))) .pipe(finalize(() => this.revokingInvoiceLoading.set(false)))
.subscribe(() => { .subscribe(() => {
this.toastService.success({ text: 'ابطال فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ابطال صورت‌حساب‌ با موفقیت انجام شد.' });
this.refreshRequested.emit(); this.refreshRequested.emit();
}); });
} }
@@ -12,7 +12,7 @@ export const posSaleInvoicesNamedRoutes: NamedRoutes<TPosSaleInvoicesRouteNames>
loadComponent: () => loadComponent: () =>
import('../../views/list.component').then((m) => m.PosSaleInvoicesComponent), import('../../views/list.component').then((m) => m.PosSaleInvoicesComponent),
meta: { meta: {
title: 'فاکتورها', title: 'صورت‌حساب‌ها',
pagePath: () => baseRoute, pagePath: () => baseRoute,
}, },
}, },
@@ -21,7 +21,7 @@ export const posSaleInvoicesNamedRoutes: NamedRoutes<TPosSaleInvoicesRouteNames>
loadComponent: () => loadComponent: () =>
import('../../views/single.component').then((m) => m.PosSaleInvoiceComponent), import('../../views/single.component').then((m) => m.PosSaleInvoiceComponent),
meta: { meta: {
title: 'جزئیات فاکتور', title: 'جزئیات صورت‌حساب‌',
pagePath: (invoiceId: string) => `${baseRoute}/${invoiceId}`, pagePath: (invoiceId: string) => `${baseRoute}/${invoiceId}`,
}, },
}, },
@@ -1,5 +1,7 @@
import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state'; import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state';
import { ISaleInvoiceFullResponse } from '@/domains/consumer/models'; import { ISaleInvoiceFullResponse } from '@/domains/consumer/models';
import { TspProviderResponseStatus } from '@/shared/catalog';
import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { catchError, finalize } from 'rxjs'; import { catchError, finalize } from 'rxjs';
import { PosSaleInvoicesService } from '../services/main.service'; import { PosSaleInvoicesService } from '../services/main.service';
@@ -9,7 +11,10 @@ interface PosSaleInvoiceState extends EntityState<ISaleInvoiceFullResponse> {}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class PosSaleInvoiceStore extends EntityStore<ISaleInvoiceFullResponse, PosSaleInvoiceState> { export class PosSaleInvoiceStore extends EntityStore<
ISaleInvoiceFullResponse,
PosSaleInvoiceState
> {
private readonly service = inject(PosSaleInvoicesService); private readonly service = inject(PosSaleInvoicesService);
constructor() { constructor() {
@@ -29,13 +34,29 @@ export class PosSaleInvoiceStore extends EntityStore<ISaleInvoiceFullResponse, P
catchError((error) => { catchError((error) => {
this.setError(error); this.setError(error);
throw error; throw error;
}), })
) )
.subscribe((entity) => { .subscribe((entity) => {
this.patchState({ entity }); this.patchState({ entity });
}); });
} }
updateStatus(status: TspProviderResponseStatus) {
this.patchState({
entity: {
...this.entity()!,
status: { value: status, translate: taxProviderStatusTranslatorUtil(status) },
},
});
}
sendToTsp(invoceId: string) {
return this.service.sendToTsp(invoceId);
}
inquiry(invoceId: string) {
return this.service.getInquiry(invoceId);
}
override reset(): void { override reset(): void {
this.setState({ this.setState({
...defaultBaseStateData, ...defaultBaseStateData,
@@ -3,6 +3,9 @@
[loading]="loading()" [loading]="loading()"
[invoice]="invoice()" [invoice]="invoice()"
[backRoute]="backRoute()" [backRoute]="backRoute()"
variant="pos" [sendToTspLoading]="sendToTspLoading()"
/> [inquiryLoading]="inquiryLoading()"
(onSendToTsp)="sendToTsp()"
(onInquiry)="inquiry()"
variant="pos" />
</div> </div>
@@ -1,7 +1,10 @@
import { ToastService } from '@/core/services/toast.service';
import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util';
import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component'; import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component';
import pageParamsUtils from '@/utils/page-params.utils'; import pageParamsUtils from '@/utils/page-params.utils';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { finalize } from 'rxjs';
import { posSaleInvoicesNamedRoutes } from '../constants'; import { posSaleInvoicesNamedRoutes } from '../constants';
import { PosSaleInvoiceStore } from '../store/main.store'; import { PosSaleInvoiceStore } from '../store/main.store';
@@ -13,15 +16,102 @@ import { PosSaleInvoiceStore } from '../store/main.store';
export class PosSaleInvoiceComponent { export class PosSaleInvoiceComponent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly store = inject(PosSaleInvoiceStore); private readonly store = inject(PosSaleInvoiceStore);
private readonly toastService = inject(ToastService);
pageParams = computed(() => pageParamsUtils(this.route)); pageParams = computed(() => pageParamsUtils(this.route));
invoiceId = signal<string>(this.pageParams()['invoiceId']); invoiceId = signal<string>(this.pageParams()['invoiceId']);
inquiryLoading = signal(false);
sendToTspLoading = signal(false);
resendToTspLoading = signal(false);
correctionLoading = signal(false);
backFromSaleLoading = signal(false);
revokeLoading = signal(false);
readonly invoice = computed(() => this.store.entity()); readonly invoice = computed(() => this.store.entity());
readonly loading = computed(() => this.store.loading()); readonly loading = computed(() => this.store.loading());
readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!()); readonly backRoute = computed(() => posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!());
sendToTsp() {
this.sendToTspLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.sendToTspLoading.set(false);
})
)
.subscribe({
next: (res) => {
this.toastService.success({ text: res.message || 'ارسال صورت‌حساب با موفقیت انجام شد.' });
this.store.updateStatus(res.status);
},
});
}
inquiry() {
this.inquiryLoading.set(true);
this.store
.inquiry(this.invoiceId())
.pipe(
finalize(() => {
this.inquiryLoading.set(false);
})
)
.subscribe({
next: (res) => {
this.toastService.success({
text: `وضعیت صورت‌حساب ${taxProviderStatusTranslatorUtil(res.status)} می‌باشد.`,
});
this.store.updateStatus(res.status);
},
});
}
resendToTsp() {
this.resendToTspLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.resendToTspLoading.set(false);
})
)
.subscribe();
}
correction() {
this.correctionLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.correctionLoading.set(false);
})
)
.subscribe();
}
backFromSale() {
this.backFromSaleLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.backFromSaleLoading.set(false);
})
)
.subscribe();
}
revoke() {
this.revokeLoading.set(true);
this.store
.sendToTsp(this.invoiceId())
.pipe(
finalize(() => {
this.revokeLoading.set(false);
})
)
.subscribe();
}
ngOnInit() { ngOnInit() {
this.store.getData(this.invoiceId()); this.store.getData(this.invoiceId());
} }
@@ -41,10 +41,13 @@
<hr /> <hr />
<div class="sticky bottom-0 flex flex-col gap-4 py-2"> <div class="sticky bottom-0 flex flex-col gap-4 py-2">
<p-card class="border-surface-border border"> <p-card class="border-surface-border border">
<div class="flex flex-col gap-4">
<field-invoice-date [control]="invoiceDate" alignment="horizontal" (onChangeDate)="changeDate($event)" />
<div class="flex shrink-0 items-center justify-between gap-2"> <div class="flex shrink-0 items-center justify-between gap-2">
<app-key-value label="مشتری" [value]="customerNameToShow()" /> <app-key-value label="مشتری" [value]="customerNameToShow()" />
<button pButton outlined icon="pi pi-pencil" size="small" (click)="openCustomerDialog()"></button> <button pButton outlined icon="pi pi-pencil" size="small" (click)="openCustomerDialog()"></button>
</div> </div>
</div>
</p-card> </p-card>
<pos-order-price-info-card /> <pos-order-price-info-card />
<button <button
@@ -1,6 +1,7 @@
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component'; // import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
// import { ICustomerResponse } from '@/modules/customers/models'; // import { ICustomerResponse } from '@/modules/customers/models';
import { KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { FieldInvoiceDateComponent } from '@/shared/components/fields/invoice_date.component';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@@ -10,7 +11,7 @@ import {
Output, Output,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormControl, FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import images from 'src/assets/images'; import images from 'src/assets/images';
@@ -33,6 +34,8 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
PosOrderCustomerDialogComponent, PosOrderCustomerDialogComponent,
KeyValueComponent, KeyValueComponent,
Card, Card,
FieldInvoiceDateComponent,
], ],
}) })
export class PosOrderSectionComponent { export class PosOrderSectionComponent {
@@ -44,6 +47,8 @@ export class PosOrderSectionComponent {
isVisibleCustomerForm = signal(false); isVisibleCustomerForm = signal(false);
invoiceDate = new FormControl(this.store.invoiceDate());
inOrderGoods = computed(() => this.store.inOrderGoods()); inOrderGoods = computed(() => this.store.inOrderGoods());
customer = computed(() => this.store.customer()); customer = computed(() => this.store.customer());
customerNameToShow = computed(() => { customerNameToShow = computed(() => {
@@ -88,4 +93,8 @@ export class PosOrderSectionComponent {
} }
submitCustomer() {} submitCustomer() {}
changeDate(date: string) {
this.store.setInvoiceDate(date);
}
} }
@@ -9,12 +9,14 @@
<qrcode [qrdata]="publicInvoiceRoute()" [width]="220" [errorCorrectionLevel]="'M'"></qrcode> <qrcode [qrdata]="publicInvoiceRoute()" [width]="220" [errorCorrectionLevel]="'M'"></qrcode>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i class="" class="pi pi-check-circle text-xl! text-green-700"></i> <i class="" class="pi pi-check-circle text-xl! text-green-700"></i>
<p class="text-lg font-bold">فاکتور شما با موفقیت ایجاد شد.</p> <p class="text-lg font-bold">صورت‌حساب‌ شما با موفقیت ایجاد شد.</p>
</div> </div>
</div> </div>
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
<button pButton type="button" outlined (click)="printInvoice()">چاپ</button> <button pButton type="button" outlined (click)="printInvoice()">چاپ</button>
<button pButton type="button" outlined (click)="sendInvoice()">ارسال به سامانه مودیان</button> <button pButton type="button" outlined [loading]="sendingLoading()" (click)="sendInvoice()">
ارسال به سامانه مودیان
</button>
<button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button> <button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button>
</div> </div>
</shared-light-bottomsheet> </shared-light-bottomsheet>
@@ -53,7 +53,7 @@ export class PosOrderSubmittedDialogComponent extends AbstractDialog {
.sendToTsp(this.invoice.id) .sendToTsp(this.invoice.id)
.pipe(finalize(() => this.sendingLoading.set(false))) .pipe(finalize(() => this.sendingLoading.set(false)))
.subscribe(() => { .subscribe(() => {
this.toastService.success({ text: 'ارسال فاکتور با موفقیت انجام شد.' }); this.toastService.success({ text: 'ارسال صورت‌حساب‌ با موفقیت انجام شد.' });
this.sended.set(true); this.sended.set(true);
this.close(); this.close();
}); });
@@ -65,7 +65,7 @@ export class PosOrderSubmittedDialogComponent extends AbstractDialog {
title: `فروشگاه ${this.posName()}`, title: `فروشگاه ${this.posName()}`,
items: [ items: [
{ {
label: 'شماره فاکتور', label: 'شماره صورت‌حساب‌',
value: this.invoice.code, value: this.invoice.code,
}, },
{ label: 'تاریخ', value: formatJalali(this.invoice.invoice_date, 'YYYY/MM/DD') }, { label: 'تاریخ', value: formatJalali(this.invoice.invoice_date, 'YYYY/MM/DD') },
@@ -7,14 +7,14 @@
(onHide)="close()"> (onHide)="close()">
@if (good) { @if (good) {
@if (isGoldMode()) { @if (isGoldMode()) {
<pos-gold-payload-form <shared-gold-payload-form
[initialValues]="goldPayload()" [initialValues]="goldPayload()"
[vatPercentage]="vatPercentage()" [vatPercentage]="vatPercentage()"
[editMode]="editMode()" [editMode]="editMode()"
[isRapidInvoice]="isRapidInvoice()" [isRapidInvoice]="isRapidInvoice()"
(onSubmit)="submit($event)" /> (onSubmit)="submit($event)" />
} @else if (isStandardMode()) { } @else if (isStandardMode()) {
<pos-standard-payload-form <shared-standard-payload-form
[initialValues]="standardPayload()" [initialValues]="standardPayload()"
[vatPercentage]="vatPercentage()" [vatPercentage]="vatPercentage()"
[measureUnit]="good.measure_unit" [measureUnit]="good.measure_unit"
@@ -1,20 +1,23 @@
import { IGoodResponse } from '@/domains/pos/models/good.io'; import { IGoodResponse } from '@/domains/pos/models/good.io';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog'; import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component'; import {
SharedGoldPayloadFormComponent,
SharedLightBottomsheetComponent,
SharedStandardPayloadFormComponent,
} from '@/shared/components';
import { IGoldPayload, IStandardPayload } from '@/shared/models';
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core'; import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { PosConfigRapidInvoiceService } from '../../../configs/components/rapidInvoice/services/main.service'; import { PosConfigRapidInvoiceService } from '../../../configs/components/rapidInvoice/services/main.service';
import { IGoldPayload, IPosOrderItem, IStandardPayload } from '../../models'; import { IPosOrderItem } from '../../models';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
import { PosGoldPayloadFormComponent } from './gold/form.component';
import { PosStandardPayloadFormComponent } from './standard/form.component';
@Component({ @Component({
selector: 'pos-payload-form-dialog', selector: 'pos-payload-form-dialog',
templateUrl: './payload-form.component.html', templateUrl: './payload-form.component.html',
imports: [ imports: [
PosGoldPayloadFormComponent, SharedGoldPayloadFormComponent,
SharedStandardPayloadFormComponent,
SharedLightBottomsheetComponent, SharedLightBottomsheetComponent,
PosStandardPayloadFormComponent,
], ],
}) })
export class PayloadFormDialogComponent extends AbstractDialog { export class PayloadFormDialogComponent extends AbstractDialog {
@@ -287,7 +287,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
if (this.selectedSettlementType() === 'CASH') { if (this.selectedSettlementType() === 'CASH') {
if (this.remainedAmount() > 0) { if (this.remainedAmount() > 0) {
return this.toastServices.warn({ return this.toastServices.warn({
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..', text: 'صورت‌حساب‌ تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
}); });
} }
} }
@@ -1,6 +1,5 @@
export * from './customer'; export * from './customer';
export * from './io'; export * from './io';
export * from './payload';
export * from './payment'; export * from './payment';
export * from './posPaymentResult'; export * from './posPaymentResult';
export * from './types'; export * from './types';
@@ -1,6 +1,6 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { IGoodResponse } from '@/domains/pos/models/good.io'; import { IGoodResponse } from '@/domains/pos/models/good.io';
import { TPosOrderGoodPayload } from './payload'; import { TPosOrderGoodPayload } from '@/shared/models';
export interface IPosOrderItem<T = TPosOrderGoodPayload> { export interface IPosOrderItem<T = TPosOrderGoodPayload> {
unit_price: number; unit_price: number;
@@ -1,9 +1,14 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
// import { ICustomerResponse } from '@/modules/customers/models'; // import { ICustomerResponse } from '@/modules/customers/models';
import { ToastService } from '@/core/services/toast.service';
import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io'; import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes'; import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils'; import {
createUUID,
formatDate,
GREGORIAN_DATE_FORMATS,
JALALI_DATE_FORMATS,
nowGregorian,
} from '@/utils';
import { computed, inject, Injectable, signal } from '@angular/core'; import { computed, inject, Injectable, signal } from '@angular/core';
import { catchError, finalize, firstValueFrom, map, throwError } from 'rxjs'; import { catchError, finalize, firstValueFrom, map, throwError } from 'rxjs';
import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models'; import { ICustomer, IPayment, IPosOrderRequest, IPosOrderResponse } from '../models';
@@ -47,7 +52,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
customerDetails: { customerDetails: {
type: CustomerType.UNKNOWN, type: CustomerType.UNKNOWN,
}, },
invoiceDate: nowJalali(JALALI_DATE_FORMATS.NUMERIC_WITH_FULL_TIME), invoiceDate: nowGregorian(JALALI_DATE_FORMATS.FULL),
payments: { payments: {
cash: 0, cash: 0,
set_off: 0, set_off: 0,
@@ -59,7 +64,6 @@ export const INITIAL_POS_STATE: IPosLandingState = {
@Injectable({ providedIn: 'any' }) @Injectable({ providedIn: 'any' })
export class PosLandingStore { export class PosLandingStore {
private readonly service = inject(PosService); private readonly service = inject(PosService);
private readonly toastServices = inject(ToastService);
private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE }); private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE });
@@ -249,6 +253,10 @@ export class PosLandingStore {
this.setState({ customerDetails: customer }); this.setState({ customerDetails: customer });
} }
setInvoiceDate(invoiceDate: string) {
this.setState({ invoiceDate });
}
setPayment(payments: IPayment) { setPayment(payments: IPayment) {
this.setState({ payments }); this.setState({ payments });
} }
@@ -259,6 +267,7 @@ export class PosLandingStore {
submitOrder() { submitOrder() {
this.setState({ submitOrderLoading: true }); this.setState({ submitOrderLoading: true });
const orderPayload: IPosOrderRequest = { const orderPayload: IPosOrderRequest = {
customer_id: this.customer().customer_id, customer_id: this.customer().customer_id,
customer_type: this.customer().type, customer_type: this.customer().type,
@@ -268,7 +277,9 @@ export class PosLandingStore {
good_id: good.id, good_id: good.id,
measure_unit: good.measure_unit, measure_unit: good.measure_unit,
})), })),
invoice_date: new Date().toISOString(), invoice_date: new Date(
formatDate(this.invoiceDate(), 'gregory', 'en', GREGORIAN_DATE_FORMATS.FULL)
).toISOString(),
payments: this.payments(), payments: this.payments(),
settlement_type: this.state$().settlement_type, settlement_type: this.state$().settlement_type,
total_amount: this.orderPricingInfo().totalAmount, total_amount: this.orderPricingInfo().totalAmount,
@@ -295,7 +306,7 @@ export class PosLandingStore {
customerDetails: { customerDetails: {
type: CustomerType.UNKNOWN, type: CustomerType.UNKNOWN,
}, },
invoiceDate: nowJalali(JALALI_DATE_FORMATS.NUMERIC), invoiceDate: _res.invoice_date,
payments: { payments: {
cash: 0, cash: 0,
set_off: 0, set_off: 0,
@@ -23,17 +23,17 @@
<!-- [style.paddingBottom]="'calc(0.75rem + {{env(safe-area-inset-bottom)}})'" --> <!-- [style.paddingBottom]="'calc(0.75rem + {{env(safe-area-inset-bottom)}})'" -->
<div class="mx-auto flex w-full max-w-3xl items-center justify-between"> <div class="mx-auto flex w-full max-w-3xl items-center justify-between">
<div class="flex flex-col items-start gap-1"> <div class="flex flex-col items-start gap-1">
<span class="text-base">مبلغ کل فاکتور</span> <span class="text-base">مبلغ کل صورت‌حساب‌</span>
<span class="text-xl font-semibold" [appPriceMask]="priceInfo().totalAmount"></span> <span class="text-xl font-semibold" [appPriceMask]="priceInfo().totalAmount"></span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-base font-medium">مشاهده فاکتور</span> <span class="text-base font-medium">مشاهده صورت‌حساب‌</span>
<i class="pi pi-chevron-up"></i> <i class="pi pi-chevron-up"></i>
</div> </div>
</div> </div>
</button> </button>
} }
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات فاکتور"> <shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات صورت‌حساب‌">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" /> <pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet> </shared-light-bottomsheet>
@@ -6,16 +6,16 @@
<div class="absolute top-0 right-0 h-full w-1" [style.backgroundColor]="preparedInfo().borderColor"></div> <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 justify-between gap-2">
<div class="text-surface-700 flex items-center gap-2"> <div class="text-surface-700 flex items-center gap-2">
<span <!-- <span
class="text-surface-700 flex h-8 w-8 items-center justify-center rounded-full border" class="text-surface-700 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border"
[style.borderColor]="preparedInfo().borderColor" [style.borderColor]="preparedInfo().borderColor"
[style.backgroundColor]="preparedInfo().borderColor + '33'"> [style.backgroundColor]="preparedInfo().borderColor + '33'">
<i class="text-sm" [ngClass]="'pi pi-' + preparedInfo().icon" [style.color]="preparedInfo().borderColor"></i> <i class="text-sm" [ngClass]="'pi pi-' + preparedInfo().icon" [style.color]="preparedInfo().borderColor"></i>
</span> </span> -->
<span class="text-surface-700 text-sm font-semibold">{{ preparedInfo().title }}</span> <span class="text-surface-700 text-sm font-semibold">{{ preparedInfo().title }}</span>
</div> </div>
<span <span
class="text-surface-700 rounded-full border px-2 py-1 text-xs font-bold" class="text-surface-700 shrink-0 rounded-full border px-2 py-1 text-xs font-bold"
[style.borderColor]="preparedInfo().borderColor"> [style.borderColor]="preparedInfo().borderColor">
{{ count || 0 }} عدد {{ count || 0 }} عدد
</span> </span>
@@ -9,7 +9,7 @@ import { Component, computed, EventEmitter, Input, Output } from '@angular/core'
imports: [CommonModule], imports: [CommonModule],
}) })
export class PosStatisticsInvoiceTypeCardComponent { export class PosStatisticsInvoiceTypeCardComponent {
@Input() type: 'all' | 'success' | 'failure' | 'not_sended' | 'pending' | 'not_original' = 'all'; @Input() type: 'all' | 'success' | 'failure' | 'not_send' | 'pending' | 'not_original' = 'all';
@Input() count?: number = 0; @Input() count?: number = 0;
@Input() totalAmount?: number = 0; @Input() totalAmount?: number = 0;
@Input() totalTaxAmount?: number = 0; @Input() totalTaxAmount?: number = 0;
@@ -48,7 +48,7 @@ export class PosStatisticsInvoiceTypeCardComponent {
bgColor: '#EF44441A', bgColor: '#EF44441A',
borderColor: '#EF4444', borderColor: '#EF4444',
}; };
case 'not_sended': case 'not_send':
return { return {
title: taxProviderStatusTranslatorUtil('NOT_SEND'), title: taxProviderStatusTranslatorUtil('NOT_SEND'),
icon: 'clock', icon: 'clock',
@@ -57,7 +57,7 @@ export class PosStatisticsInvoiceTypeCardComponent {
}; };
case 'pending': case 'pending':
return { return {
title: 'انتظار ارسال', title: taxProviderStatusTranslatorUtil('FISCAL_QUEUED'),
icon: 'clock', icon: 'clock',
bgColor: '#3B82F61A', bgColor: '#3B82F61A',
borderColor: '#3B82F6', borderColor: '#3B82F6',
+1 -1
View File
@@ -3,7 +3,7 @@ export interface IPosStatisticsRawResponse {
success: IPosStatisticsItem; success: IPosStatisticsItem;
failure: IPosStatisticsItem; failure: IPosStatisticsItem;
pending: IPosStatisticsItem; pending: IPosStatisticsItem;
not_sended: IPosStatisticsItem; not_send: IPosStatisticsItem;
not_original: IPosStatisticsItem; not_original: IPosStatisticsItem;
} }
export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {} export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {}
@@ -30,12 +30,12 @@
[count]="item()?.failure?.count" [count]="item()?.failure?.count"
(onClick)="toInvoicesPage('failure')" /> (onClick)="toInvoicesPage('failure')" />
<pos-statistics-invoice-type-card <pos-statistics-invoice-type-card
type="not_sended" type="not_send"
[loading]="loading()" [loading]="loading()"
[totalAmount]="item()?.not_sended?.total_amount" [totalAmount]="item()?.not_send?.total_amount"
[totalTaxAmount]="item()?.not_sended?.total_tax" [totalTaxAmount]="item()?.not_send?.total_tax"
[count]="item()?.not_sended?.count" [count]="item()?.not_send?.count"
(onClick)="toInvoicesPage('not_sended')" /> (onClick)="toInvoicesPage('not_send')" />
<pos-statistics-invoice-type-card <pos-statistics-invoice-type-card
type="pending" type="pending"
[loading]="loading()" [loading]="loading()"
@@ -62,11 +62,11 @@ export class PosStatisticsRootComponent {
case 'failure': case 'failure':
queryParams.status = 'FAILURE'; queryParams.status = 'FAILURE';
break; break;
case 'notSended': case 'not_send':
queryParams.status = 'NOT_SEND'; queryParams.status = 'NOT_SEND';
break; break;
case 'pending': case 'pending':
queryParams.status = 'QUEUED'; queryParams.status = 'FISCAL_QUEUED';
break; break;
} }
const queryString = new URLSearchParams(queryParams as Record<string, string>).toString(); const queryString = new URLSearchParams(queryParams as Record<string, string>).toString();
@@ -4,15 +4,14 @@
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ width: '500px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<uikit-datepicker <!-- <uikit-datepicker
label="تاریخ شروع لایسنس" label="تاریخ شروع لایسنس"
[control]="form.controls.starts_at" [control]="form.controls.starts_at"
name="starts_at" name="starts_at"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/> /> -->
<!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> --> <!-- <uikit-datepicker label="تاریخ انقضا لایسنس" [control]="form.controls.expires_at" name="expires_at" /> -->
<app-partner-select label="شریک تجاری" [control]="form.controls.partner_id" [showClear]="true" /> <app-partner-select label="شریک تجاری" [control]="form.controls.partner_id" [showClear]="true" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
@@ -1,7 +1,7 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component'; import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; // import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, computed, inject, Input } from '@angular/core'; import { Component, computed, inject, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { PartnerSelectComponent } from '../../../partners/shared/select.component'; import { PartnerSelectComponent } from '../../../partners/shared/select.component';
@@ -15,7 +15,7 @@ import { LicensesService } from '../../services/licenses.service';
ReactiveFormsModule, ReactiveFormsModule,
SharedDialogComponent, SharedDialogComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
UikitFlatpickrJalaliComponent, // UikitFlatpickrJalaliComponent,
PartnerSelectComponent, PartnerSelectComponent,
], ],
}) })
@@ -4,16 +4,15 @@
[modal]="true" [modal]="true"
[style]="{ width: '300px' }" [style]="{ width: '300px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="تعداد شارژ" [control]="form.controls.quantity" type="number" /> <app-input label="تعداد شارژ" [control]="form.controls.quantity" type="number" />
<uikit-datepicker <!-- <uikit-datepicker
label="انقضای فروش" label="انقضای فروش"
[control]="form.controls.activated_expires_at" [control]="form.controls.activated_expires_at"
name="activated_expires_at" name="activated_expires_at"
[minDate]="minDate" [minDate]="minDate"
/> /> -->
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
</shared-dialog> </shared-dialog>
@@ -1,13 +1,13 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; // import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { nowJalali } from '@/utils'; import { nowJalali } from '@/utils';
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ReactiveFormsModule, Validators } from '@angular/forms';
import { IPartnerChargeLicenseTransactionRequest } from '../models/chargeLicenseTransactions_io'; import { IPartnerChargeLicenseTransactionRequest } from '../models/chargeLicenseTransactions_io';
import { AdminPartnerChargeLicenseTransactionsService } from '../services/chargeLicenseTransactions.service'; import { AdminPartnerChargeLicenseTransactionsService } from '../services/chargeLicenseTransactions.service';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
@Component({ @Component({
selector: 'partner-charge-license-form-dialog', selector: 'partner-charge-license-form-dialog',
@@ -17,7 +17,7 @@ import { SharedDialogComponent } from '@/shared/components/dialog/dialog.compone
ReactiveFormsModule, ReactiveFormsModule,
InputComponent, InputComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
UikitFlatpickrJalaliComponent, // UikitFlatpickrJalaliComponent,
], ],
}) })
export class PartnerChargeLicenseFormDialogComponent extends AbstractFormDialog< export class PartnerChargeLicenseFormDialogComponent extends AbstractFormDialog<
@@ -4,16 +4,15 @@
[modal]="true" [modal]="true"
[style]="{ width: '300px' }" [style]="{ width: '300px' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()">
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4"> <form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="تعداد شارژ" [control]="form.controls.quantity" type="number" /> <app-input label="تعداد شارژ" [control]="form.controls.quantity" type="number" />
<uikit-datepicker <!-- <uikit-datepicker
label="انقضای فروش" label="انقضای فروش"
[control]="form.controls.activated_expires_at" [control]="form.controls.activated_expires_at"
name="activated_expires_at" name="activated_expires_at"
[minDate]="minDate" [minDate]="minDate"
/> /> -->
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
</shared-dialog> </shared-dialog>
@@ -1,13 +1,13 @@
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; // import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
import { nowJalali } from '@/utils'; import { nowJalali } from '@/utils';
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ReactiveFormsModule, Validators } from '@angular/forms';
import { IAdminPartnerChargeAccountRequest } from '../../models/chargeAccount_io'; import { IAdminPartnerChargeAccountRequest } from '../../models/chargeAccount_io';
import { AdminPartnerChargeAccountService } from '../../services/chargeAccount.service'; import { AdminPartnerChargeAccountService } from '../../services/chargeAccount.service';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
@Component({ @Component({
selector: 'admin-partner-charge-account-form-dialog', selector: 'admin-partner-charge-account-form-dialog',
@@ -17,7 +17,7 @@ import { SharedDialogComponent } from '@/shared/components/dialog/dialog.compone
ReactiveFormsModule, ReactiveFormsModule,
InputComponent, InputComponent,
FormFooterActionsComponent, FormFooterActionsComponent,
UikitFlatpickrJalaliComponent, // UikitFlatpickrJalaliComponent,
], ],
}) })
export class AdminPartnerChargeAccountFormDialogComponent extends AbstractFormDialog< export class AdminPartnerChargeAccountFormDialogComponent extends AbstractFormDialog<
@@ -9,7 +9,7 @@ export const publicSaleInvoicesNamedRoutes: NamedRoutes<TPublicSaleInvoicesRoute
loadComponent: () => loadComponent: () =>
import('../../views/single.component').then((m) => m.PublicSaleInvoiceComponent), import('../../views/single.component').then((m) => m.PublicSaleInvoiceComponent),
meta: { meta: {
title: 'جزئیات فاکتور', title: 'جزئیات صورت‌حساب‌',
pagePath: (invoiceId: string) => `sale-invoices/${invoiceId}`, pagePath: (invoiceId: string) => `sale-invoices/${invoiceId}`,
}, },
}, },
@@ -1,5 +1,13 @@
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { Component, EventEmitter, inject, Input, Output, signal } from '@angular/core'; import {
Component,
EventEmitter,
inject,
Input,
Output,
signal,
SimpleChanges,
} from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { finalize, Observable } from 'rxjs'; import { finalize, Observable } from 'rxjs';
@@ -43,7 +51,7 @@ export abstract class AbstractForm<
// }); // });
} }
ngOnChanges() { ngOnChanges(changes: SimpleChanges) {
this.form.patchValue(this.initialValues ?? this.defaultValues); this.form.patchValue(this.initialValues ?? this.defaultValues);
} }
+1 -2
View File
@@ -8,6 +8,5 @@
[showClear]="showClear" [showClear]="showClear"
[name]="name || type" [name]="name || type"
[overlayOptions]="overlayOptions()" [overlayOptions]="overlayOptions()"
appendTo="body" appendTo="body" />
/>
</uikit-field> </uikit-field>
@@ -0,0 +1,3 @@
export * from './invoice-settlement-type-tag.component';
export * from './invoice-settlement-type-translator.util';
export * from './type';
@@ -0,0 +1 @@
<p-tag [severity]="severity" size="large" [value]="translate || label"></p-tag>
@@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';
import { Tag } from 'primeng/tag';
import taxProviderStatusTranslatorUtil from './invoice-settlement-type-translator.util';
import { InvoiceSettlementType } from './type';
@Component({
selector: 'catalog-invoice-settlement-type-tag',
templateUrl: './invoice-settlement-type-tag.component.html',
imports: [Tag],
})
export class CatalogInvoiceSettlementTypeTagComponent {
@Input({ required: true }) type!: InvoiceSettlementType;
@Input() translate!: string;
get severity() {
switch (this.type) {
case 'CASH':
return 'success';
case 'CREDIT':
return 'secondary';
case 'MIXED':
return 'warn';
default:
return 'secondary';
}
}
get label() {
return taxProviderStatusTranslatorUtil(this.type);
}
}
@@ -0,0 +1,14 @@
import { InvoiceSettlementType } from './type';
export default (status: InvoiceSettlementType) => {
switch (status) {
case 'CASH':
return 'نقدی';
case 'CREDIT':
return 'نسیه';
case 'MIXED':
return 'نقدی / نسیه';
default:
return 'نامشخص';
}
};
@@ -0,0 +1,8 @@
export const InvoiceSettlementType = {
CASH: 'CASH',
CREDIT: 'CREDIT',
MIXED: 'MIXED',
} as const;
export type InvoiceSettlementType =
(typeof InvoiceSettlementType)[keyof typeof InvoiceSettlementType];
@@ -3,11 +3,11 @@ import { Component, Input } from '@angular/core';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
@Component({ @Component({
selector: 'pos-form-dialog-amount-card-template', selector: 'shared-calculated-amount-card',
templateUrl: './form-dialog-amount-card-template.component.html', templateUrl: './calculated-amount-card.component.html',
imports: [Card, PriceMaskDirective], imports: [Card, PriceMaskDirective],
}) })
export class PosFormDialogAmountCardTemplateComponent { export class CalculatedAmountCardComponent {
@Input({ required: true }) baseTotalAmount!: number; @Input({ required: true }) baseTotalAmount!: number;
@Input({ required: true }) totalAmount!: number; @Input({ required: true }) totalAmount!: number;
@Input({ required: true }) discountAmount!: number; @Input({ required: true }) discountAmount!: number;
@@ -1,7 +1,7 @@
<p-confirmdialog #cd> <p-confirmdialog #cd>
<ng-template #headless let-message let-onAccept="onAccept" let-onReject="onReject"> <ng-template #headless let-message let-onAccept="onAccept" let-onReject="onReject">
@if (message) { @if (message) {
<div class="bg-surface-0 dark:bg-surface-900 flex flex-col items-center rounded p-8"> <div class="bg-surface-card flex flex-col items-center rounded p-8">
<div <div
class="bg-primary text-primary-contrast -mt-20 inline-flex h-24 w-24 items-center justify-center rounded-full"> class="bg-primary text-primary-contrast -mt-20 inline-flex h-24 w-24 items-center justify-center rounded-full">
<i class="pi pi-question text-5xl!"></i> <i class="pi pi-question text-5xl!"></i>
@@ -9,11 +9,19 @@
<span class="mt-6 mb-2 block text-2xl font-bold">{{ message.header }}</span> <span class="mt-6 mb-2 block text-2xl font-bold">{{ message.header }}</span>
<p class="mb-0">{{ message.message }}</p> <p class="mb-0">{{ message.message }}</p>
<div class="mt-6 flex items-center gap-2"> <div class="mt-6 flex items-center gap-2">
<p-button [label]="message.acceptLabel" type="button" (onClick)="onAccept()" styleClass="w-32"></p-button> <p-button
[label]="message.acceptLabel"
type="button"
[loading]="!!message.acceptLoading"
[disabled]="!!message.acceptLoading"
[severity]="message.acceptButtonProps?.severity"
(onClick)="onAccept()"
styleClass="w-32"></p-button>
<p-button <p-button
[label]="message.rejectLabel" [label]="message.rejectLabel"
type="button" type="button"
[outlined]="true" [outlined]="true"
[disabled]="!!message.acceptLoading"
(onClick)="onReject()" (onClick)="onReject()"
styleClass="w-32"></p-button> styleClass="w-32"></p-button>
</div> </div>
@@ -1,13 +1,9 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Confirmation, ConfirmationService } from 'primeng/api'; import { Confirmation, ConfirmationService } from 'primeng/api';
export interface AppConfirmOptions { export interface AppConfirmOptions extends Confirmation {
header: string; variant?: 'warn' | 'danger' | 'default';
message: string; acceptLoading?: boolean;
acceptLabel?: string;
rejectLabel?: string;
accept?: () => void;
reject?: () => void;
} }
@Injectable({ @Injectable({
@@ -16,16 +12,38 @@ export interface AppConfirmOptions {
export class AppConfirmationService { export class AppConfirmationService {
private confirmationService = inject(ConfirmationService); private confirmationService = inject(ConfirmationService);
ask(options: Confirmation): Promise<boolean> { ask(options: AppConfirmOptions): Promise<boolean> {
const { variant = 'default', ...confirmationOptions } = options;
switch (variant) {
case 'danger':
options.acceptButtonProps.severity = 'danger';
break;
case 'warn':
options.acceptButtonProps.severity = 'warn';
break;
}
return new Promise((resolve) => { return new Promise((resolve) => {
this.confirmationService.confirm({ this.confirmationService.confirm({
position: 'bottom', position: 'bottom',
acceptLabel: 'تایید', acceptLabel: 'تایید',
rejectLabel: 'لغو', rejectLabel: 'لغو',
...options,
closeOnEscape: true, closeOnEscape: true,
accept: () => options.accept?.() || resolve(true), ...confirmationOptions,
reject: () => resolve(false), accept: async () => {
try {
options.acceptLoading = true;
await confirmationOptions.accept?.();
resolve(true);
} finally {
options.acceptLoading = false;
}
},
reject: () => {
if (options.acceptLoading) return;
resolve(false);
},
}); });
}); });
} }
@@ -0,0 +1,2 @@
export * from './dialog.component';
export * from './light-bottomsheet.component';
@@ -0,0 +1,31 @@
import { DatepickerComponent } from '@/uikit';
import { formatGregorian, gregorianAddUnit } from '@/utils';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'field-invoice-date',
template: `
<app-datepicker
label="تاریخ صورت‌حساب‌"
[alignment]="alignment"
[min]="invoiceMinDate"
[max]="invoiceMaxDate"
[control]="control"
(valueChange)="changeDate($event)"
/>
`,
imports: [DatepickerComponent],
})
export class FieldInvoiceDateComponent {
@Input() alignment: 'horizontal' | 'vertical' = 'vertical';
@Input({ required: true }) control!: FormControl<string | null>;
@Output() onChangeDate = new EventEmitter<string>();
invoiceMinDate = formatGregorian(gregorianAddUnit(new Date(), -7, 'days'));
invoiceMaxDate = formatGregorian(new Date());
changeDate(date: string) {
this.onChangeDate.emit(date);
}
}
@@ -5,7 +5,7 @@ import { InputComponent } from '../input/input.component';
@Component({ @Component({
selector: 'field-invoice-number-sequence', selector: 'field-invoice-number-sequence',
template: `<app-input template: `<app-input
label="شروع شماره فاکتور" label="شروع شماره صورت‌حساب‌"
[control]="control" [control]="control"
[name]="name" [name]="name"
type="number" type="number"
@@ -15,5 +15,5 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
export class FieldInvoiceTemplatesComponent { export class FieldInvoiceTemplatesComponent {
@Input({ required: true }) control = new FormControl<string>(''); @Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'invoice-template'; @Input() name = 'invoice-template';
@Input() label = 'قالب فاکتور'; @Input() label = 'قالب صورت‌حساب‌';
} }
@@ -1,16 +1,16 @@
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'field-license-starts-at', selector: 'field-license-starts-at',
template: `<uikit-datepicker // template: `<uikit-datepicker
label="تاریخ شروع لایسنس" // label="تاریخ شروع لایسنس"
[control]="control" // [control]="control"
[name]="name" // [name]="name"
hint="مدت زمان لایسنس‌ها ۱ ساله هستند." // hint="مدت زمان لایسنس‌ها ۱ ساله هستند."
/>`, // />`,
imports: [ReactiveFormsModule, UikitFlatpickrJalaliComponent], template: '',
imports: [ReactiveFormsModule],
}) })
export class LicenseStartsAtComponent { export class LicenseStartsAtComponent {
@Input({ required: true }) control = new FormControl<string>(''); @Input({ required: true }) control = new FormControl<string>('');
+4 -1
View File
@@ -1,12 +1,15 @@
// export * from './abstract-select.component'; // export * from './abstract-select.component';
export * from './amountPercentageInput/amount-percentage-input.component';
export * from './breadcrumb.component'; export * from './breadcrumb.component';
export * from './calculatedAmountCard/calculated-amount-card.component';
export * from './card-data.component'; export * from './card-data.component';
export * from './changePasswordFormDialog/change-password-form-dialog.component'; export * from './changePasswordFormDialog/change-password-form-dialog.component';
export * from './dialog/dialog.component'; export * from './dialog';
export * from './fields'; export * from './fields';
export * from './inlineConfirmation/inline-confirmation.component'; export * from './inlineConfirmation/inline-confirmation.component';
export * from './inlineEdit/inline-edit.component'; export * from './inlineEdit/inline-edit.component';
export * from './input/input.component'; export * from './input/input.component';
export * from './key-value.component/key-value.component'; export * from './key-value.component/key-value.component';
export * from './passwordInput/password-input.component'; export * from './passwordInput/password-input.component';
export * from './posPayment';
export * from './table-action-row.component'; export * from './table-action-row.component';
@@ -62,7 +62,9 @@
@if (suffixTemp) { @if (suffixTemp) {
<ng-container [ngTemplateOutlet]="suffixTemp" /> <ng-container [ngTemplateOutlet]="suffixTemp" />
} @else if (preparedSuffix()) { } @else if (preparedSuffix()) {
<p-inputgroup-addon>{{ preparedSuffix() }}</p-inputgroup-addon> <p-inputgroup-addon [ngClass]="{ 'border-red-400!': (control.touched || control.dirty) && control.invalid }">{{
preparedSuffix()
}}</p-inputgroup-addon>
} }
</p-inputgroup> </p-inputgroup>
} }
@@ -0,0 +1,18 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<p-message severity="info" icon="pi pi-info-circle">
در برگشت از فروش صورتحساب شما میتوانید صرفا تعداد محصولات و خدمات خود را کم و حذف کنید و حداقل مقدار محصولات و خدمات
شما ۱ می باشد.
</p-message>
<field-invoice-date [control]="form.controls.invoice_date" />
@for (item of items.controls; track (goodItems()[$index]?.good_snapshot!.id || '') + ($index + '')) {
<div class="py-2">
<shared-return-form-item
[control]="item.controls.quantity"
[good]="goodItems()[$index]!.good_snapshot"
[index]="$index" />
</div>
}
<app-form-footer-actions />
</form>
@@ -0,0 +1,108 @@
import { Maybe } from '@/core';
import { AbstractForm } from '@/shared/abstractClasses';
import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils';
import { Component, computed, Input, signal, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Message } from 'primeng/message';
import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component';
import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component';
import { IInvoiceItem } from '../sale-invoice-full-response.model';
import { SharedReturnFormItemComponent } from './item-form.component';
type ItemForm = FormGroup<{
quantity: FormControl<number | null>;
}>;
type BackFromSaleFormValue = {
items: {
id: string;
quantity: number;
maxQuantity: number;
}[];
};
@Component({
selector: 'shared-return-form',
templateUrl: 'form.component.html',
imports: [
Message,
FormFooterActionsComponent,
ReactiveFormsModule,
FieldInvoiceDateComponent,
SharedReturnFormItemComponent,
],
})
export class SharedReturnFormComponent extends AbstractForm<
BackFromSaleFormValue,
any,
IInvoiceItem[]
> {
@Input({ required: true }) invoiceDate!: string;
form = this.fb.group({
invoice_date: [this.invoiceDate],
items: this.fb.array<ItemForm>([], {
// validators: [atLeastOneQuantityValidator],
}),
});
goodItems = computed(() => this.initialValues!);
get items() {
return this.form.controls.items;
}
preparedCalculation = signal<Maybe<any>>(null);
init() {
this.initForm();
this.prepareCalculation();
}
prepareCalculation() {
const calculated = [];
this.initialValues?.forEach((item) => {});
}
initForm() {
this.items.clear();
this.form.controls.invoice_date.setValue(
formatDate(this.invoiceDate, 'gregory', 'en', GREGORIAN_DATE_FORMATS.FULL)
);
this.initialValues?.forEach((item: any) => {
this.items.push(
this.fb.group({
quantity: [
Number(item.quantity),
[Validators.required, Validators.min(0), Validators.max(Number(item.quantity))],
],
})
);
});
}
override ngOnInit(): void {
this.init();
}
override ngOnChanges(changes: SimpleChanges) {
if (changes['initialValues']) {
this.init();
}
}
override submitForm() {
const payload = this.items.controls.map((control) => ({
id: control.get('id')?.value,
quantity: control.get('quantity')?.value,
}));
if (!payload.some((item) => item.quantity)) {
this.toastService.warn({
text: 'حداقل مقدار یکی از کالاها/خدمات باید بیشتر از ۰ باشد.',
});
return;
}
}
}
@@ -0,0 +1,13 @@
<div>
<p-divider align="right">
{{ index + 1 }}- <b>{{ good.name }}</b>
</p-divider>
<app-input
[control]="control"
name="quantity"
[label]="good.measure_unit.name"
type="number"
[suffix]="good.measure_unit.name"
[max]="control.defaultValue || undefined"
[min]="0" />
</div>
@@ -0,0 +1,45 @@
import { Maybe } from '@/core';
import { Component, Input, signal, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Divider } from 'primeng/divider';
import { InputComponent } from '../../input/input.component';
import { Goodsnapshot } from '../sale-invoice-full-response.model';
type ItemForm = FormGroup<{
quantity: FormControl<number | null>;
}>;
type BackFromSaleFormValue = {
items: {
id: string;
quantity: number;
maxQuantity: number;
}[];
};
@Component({
selector: 'shared-return-form-item',
templateUrl: './item-form.component.html',
imports: [InputComponent, ReactiveFormsModule, Divider],
})
export class SharedReturnFormItemComponent {
@Input({ required: true }) control!: FormControl<number | null>;
@Input({ required: true }) good!: Goodsnapshot;
@Input({ required: true }) index!: number;
preparedCalculation = signal<Maybe<any>>(null);
prepareCalculation() {
const calculated = [];
}
ngOnInit(): void {
this.prepareCalculation();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['initialValues']) {
this.prepareCalculation();
}
}
}
@@ -1,6 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { InvoiceTypes, TspProviderResponseStatus } from '@/shared/catalog'; import { InvoiceTypes, TspProviderResponseStatus } from '@/shared/catalog';
import { InvoiceSettlementType } from '@/shared/catalog/invoiceSettlementType';
import { IEnumTranslate } from '@/shared/models/enum_translate.type'; import { IEnumTranslate } from '@/shared/models/enum_translate.type';
export interface ISaleInvoiceFullRawResponse { export interface ISaleInvoiceFullRawResponse {
@@ -13,7 +14,7 @@ export interface ISaleInvoiceFullRawResponse {
consumer_account: ConsumerAccount; consumer_account: ConsumerAccount;
pos: Pos; pos: Pos;
payments: Payment[]; payments: Payment[];
items: Item[]; items: IInvoiceItem[];
invoice_number: number; invoice_number: number;
type: IEnumTranslate<InvoiceTypes>; type: IEnumTranslate<InvoiceTypes>;
created_at: string; created_at: string;
@@ -24,11 +25,12 @@ export interface ISaleInvoiceFullRawResponse {
tax_id: Maybe<string>; tax_id: Maybe<string>;
reference_invoice: Maybe<string>; reference_invoice: Maybe<string>;
notes: Maybe<string>; notes: Maybe<string>;
settlement_type: IEnumTranslate<InvoiceSettlementType>;
} }
export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {} export interface ISaleInvoiceFullResponse extends ISaleInvoiceFullRawResponse {}
interface Item { export interface IInvoiceItem {
id: string; id: string;
good_id: string; good_id: string;
service_id: null; service_id: null;
@@ -62,9 +64,7 @@ interface Category {
updated_at: string; updated_at: string;
} }
interface Goodsnapshot { export interface Goodsnapshot extends Good {}
good: Good;
}
interface Good { interface Good {
id: string; id: string;
@@ -3,8 +3,14 @@
} @else if (!invoice) { } @else if (!invoice) {
<uikit-empty-state> </uikit-empty-state> <uikit-empty-state> </uikit-empty-state>
} @else { } @else {
<shared-light-bottomsheet [visible]="actionLoading" [closable]="false" header="لطفا صبر کنید">
<div class="flex h-[30svh] flex-col items-center justify-center gap-6">
<p-progressSpinner />
<span class="text-lg font-bold">در حال ارسال درخواست شما...</span>
</div>
</shared-light-bottomsheet>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات فاکتور" [editable]="false" [backRoute]="backRoute"> <app-card-data cardTitle="اطلاعات صورت‌حساب‌" [editable]="false" [backRoute]="backRoute">
<ng-template #moreActions> <ng-template #moreActions>
<div class=""> <div class="">
<p-menu #menu [model]="moreActionMenuItems()" [popup]="true" appendTo="body" /> <p-menu #menu [model]="moreActionMenuItems()" [popup]="true" appendTo="body" />
@@ -14,8 +20,9 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="listKeyValue"> <div class="listKeyValue">
<app-key-value label="شماره صورت‌حساب" [value]="invoice.invoice_number" /> <app-key-value label="شماره صورت‌حساب" [value]="invoice.invoice_number" />
<app-key-value label="تاریخ فاکتور" [value]="invoice.invoice_date" type="dateTime" /> <app-key-value label="شماره منحصر به فرد مالیاتی" [value]="invoice.tax_id" />
<app-key-value label="تاریخ ایجاد فاکتور" [value]="invoice.created_at" type="dateTime" /> <app-key-value label="تاریخ صورت‌حساب‌" [value]="invoice.invoice_date" type="date" />
<app-key-value label="تاریخ ایجاد صورت‌حساب‌" [value]="invoice.created_at" type="dateTime" />
<app-key-value label="نوع صورت‌حساب"> <app-key-value label="نوع صورت‌حساب">
<catalog-invoice-type-tag [status]="invoice.type.value" /> <catalog-invoice-type-tag [status]="invoice.type.value" />
</app-key-value> </app-key-value>
@@ -26,11 +33,18 @@
<app-key-value label="توضیحات" [value]="invoice.notes || '----'" /> <app-key-value label="توضیحات" [value]="invoice.notes || '----'" />
</div> </div>
</div> </div>
<p-divider align="center"> اطلاعات پرداخت </p-divider> <p-divider align="center"> اطلاعات مالی</p-divider>
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3"> <div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
<app-key-value label="مبلغ تخفیف" [value]="invoice.discount_amount" type="price" /> <app-key-value label="مبلغ تخفیف" [value]="invoice.discount_amount" type="price" />
<app-key-value label="مبلغ مالیات" [value]="invoice.tax_amount" type="price" /> <app-key-value label="مبلغ مالیات" [value]="invoice.tax_amount" type="price" />
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
</div>
<p-divider align="center"> اطلاعات پرداخت</p-divider>
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
<app-key-value label="نحوه‌ی تسویه‌حساب">
<catalog-invoice-settlement-type-tag [type]="invoice.settlement_type.value" />
</app-key-value>
@for (payment of invoice.payments; track $index) { @for (payment of invoice.payments; track $index) {
<app-key-value <app-key-value
@@ -96,4 +110,8 @@
</app-page-data-list> </app-page-data-list>
</app-card-data> </app-card-data>
</div> </div>
<shared-light-bottomsheet [(visible)]="showBackFromSaleForm" header=" برگشت از فروش">
<shared-return-form [initialValues]="invoice.items" [invoiceDate]="invoice.invoice_date" />
</shared-light-bottomsheet>
} }
@@ -7,7 +7,12 @@ import {
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
CatalogTaxProviderStatusTagComponent, CatalogTaxProviderStatusTagComponent,
} from '@/shared/catalog'; } from '@/shared/catalog';
import { AppCardComponent, KeyValueComponent } from '@/shared/components'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import {
AppCardComponent,
KeyValueComponent,
SharedLightBottomsheetComponent,
} from '@/shared/components';
import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { import {
@@ -34,8 +39,11 @@ import { MenuItem } from 'primeng/api';
import { Button } from 'primeng/button'; import { Button } from 'primeng/button';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { Menu } from 'primeng/menu'; import { Menu } from 'primeng/menu';
import { ProgressSpinner } from 'primeng/progressspinner';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Observable } from 'rxjs';
import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service';
import { SharedReturnFormComponent } from './returnForm/form.component';
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
@@ -55,6 +63,10 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
Menu, Menu,
Button, Button,
CatalogInvoiceSettlementTypeTagComponent,
SharedLightBottomsheetComponent,
ProgressSpinner,
SharedReturnFormComponent,
], ],
}) })
export class SharedSaleInvoiceSingleViewComponent { export class SharedSaleInvoiceSingleViewComponent {
@@ -69,12 +81,19 @@ export class SharedSaleInvoiceSingleViewComponent {
@Input() invoice!: Maybe<ISaleInvoiceFullResponse>; @Input() invoice!: Maybe<ISaleInvoiceFullResponse>;
@Input() variant: TSaleInvoiceSingleViewVariant = 'full'; @Input() variant: TSaleInvoiceSingleViewVariant = 'full';
@Input() backRoute?: UrlTree | string | any[]; @Input() backRoute?: UrlTree | string | any[];
@Input() sendToTspLoading: boolean = false;
@Input() resendToTspLoading: boolean = false;
@Input() inquiryLoading: boolean = false;
@Output() onRefresh = new EventEmitter<void>(); @Output() onRefresh = new EventEmitter<void>();
@Output() onSendToTsp = new EventEmitter<Observable<any>>();
@Output() onInquiry = new EventEmitter<Observable<any>>();
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>; @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
moreActionMenuItems = signal<MenuItem[]>([]); moreActionMenuItems = signal<MenuItem[]>([]);
showCorrectionForm = signal(false);
showBackFromSaleForm = signal(false);
constructor() { constructor() {
this.showErrors = this.showErrors.bind(this); this.showErrors = this.showErrors.bind(this);
@@ -82,22 +101,32 @@ export class SharedSaleInvoiceSingleViewComponent {
this.showCorrection = this.showCorrection.bind(this); this.showCorrection = this.showCorrection.bind(this);
this.showBackFromSale = this.showBackFromSale.bind(this); this.showBackFromSale = this.showBackFromSale.bind(this);
this.printInvoice = this.printInvoice.bind(this); this.printInvoice = this.printInvoice.bind(this);
this.send = this.send.bind(this);
this.inquiry = this.inquiry.bind(this);
}
get actionLoading() {
return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading;
} }
showErrors() {} showErrors() {}
showRevokeConfirmation() { inquiry() {
console.log('this.confirmationService', this.confirmationService); this.onInquiry.emit();
}
this.confirmationService.ask({ send() {
header: `ابطال فاکتور شماره ${this.invoice!.invoice_number}`, this.onSendToTsp.emit();
message: `در صورت تایید ابطال فاکتور شماره ${this.invoice!.invoice_number} بر روی دکمه‌ی ابطال کلیک کنید.`, }
async showRevokeConfirmation() {
await this.confirmationService.ask({
header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`,
message: `در صورت تایید ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number} بر روی دکمه‌ی ابطال کلیک کنید.`,
acceptLabel: 'ابطال', acceptLabel: 'ابطال',
acceptButtonProps: { acceptButtonProps: {
severity: 'dangerdock', severity: 'dangerdock',
}, },
variant: 'danger',
accept: () => { accept: async () => {
console.log('here');
// this.deferredInstallPrompt.prompt(); // this.deferredInstallPrompt.prompt();
// this.deferredInstallPrompt.userChoice.finally(() => { // this.deferredInstallPrompt.userChoice.finally(() => {
// this.deferredInstallPrompt = null; // this.deferredInstallPrompt = null;
@@ -109,12 +138,26 @@ export class SharedSaleInvoiceSingleViewComponent {
}); });
} }
showCorrection() {} showCorrection() {}
showBackFromSale() {} showBackFromSale() {
this.showBackFromSaleForm.set(true);
}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['invoice'] && this.invoice) { if (changes['invoice'] && this.invoice) {
const invoiceStatus = this.invoice.status.value; const invoiceStatus = this.invoice.status.value;
const actions: MenuItem[] = [ const actions: MenuItem[] = [
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: 'NOT_SEND',
command: this.send,
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: 'FISCAL_QUEUED',
command: this.inquiry,
},
{ {
label: 'دلایل خطا', label: 'دلایل خطا',
icon: 'pi pi-question', icon: 'pi pi-question',
@@ -284,21 +327,18 @@ export class SharedSaleInvoiceSingleViewComponent {
{ {
label: 'اجرت', label: 'اجرت',
value: formatWithCurrency(item.payload.wages), value: formatWithCurrency(item.payload.wages),
show: show: mustPrintConfig.items?.wage && item.good_snapshot.pricing_model === 'GOLD',
mustPrintConfig.items?.wage && item.good_snapshot.good.pricing_model === 'GOLD',
}, },
{ {
label: 'سود', label: 'سود',
value: formatWithCurrency(item.payload.wages), value: formatWithCurrency(item.payload.wages),
show: show: mustPrintConfig.items?.profit && item.good_snapshot.pricing_model === 'GOLD',
mustPrintConfig.items?.profit && item.good_snapshot.good.pricing_model === 'GOLD',
}, },
{ {
label: 'حق‌العمل', label: 'حق‌العمل',
value: formatWithCurrency(item.payload.commission), value: formatWithCurrency(item.payload.commission),
show: show:
mustPrintConfig.items?.commission && mustPrintConfig.items?.commission && item.good_snapshot.pricing_model === 'GOLD',
item.good_snapshot.good.pricing_model === 'GOLD',
}, },
{ {
label: 'مالیات بر ارزش افزوده', label: 'مالیات بر ارزش افزوده',
@@ -1,3 +1,4 @@
import { Maybe } from '@/core';
import { formatDurationToText, formatJalali, JALALI_DATE_FORMATS } from '@/utils'; import { formatDurationToText, formatJalali, JALALI_DATE_FORMATS } from '@/utils';
import priceMaskUtils from '@/utils/price-mask.utils'; import priceMaskUtils from '@/utils/price-mask.utils';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -16,7 +17,7 @@ import { TDataType } from '../pageDataList/page-data-list.component';
}) })
export class KeyValueComponent { export class KeyValueComponent {
@Input() label!: string; @Input() label!: string;
@Input() value?: string | boolean | number; @Input() value?: Maybe<string | boolean | number>;
@Input() hint?: string; @Input() hint?: string;
@Input() alignment?: 'start' | 'center' | 'end' = 'start'; @Input() alignment?: 'start' | 'center' | 'end' = 'start';
@Input() type: TDataType = 'simple'; @Input() type: TDataType = 'simple';
@@ -0,0 +1,46 @@
import { IPosOrderItem } from '@/domains/pos/modules/shop/models';
import { goldDiscountType, TGoldDiscountType } from '@/shared/localEnum/constants/goldDiscountType';
import { IGoldPayload } from '@/shared/models';
export interface IGoldAmountCalculationPayload {
commission: string;
wages: string;
discount_amount: string;
profit: string;
unit_price: string;
quantity: string;
discount_type: TGoldDiscountType;
}
export default (payload: Partial<IPosOrderItem<IGoldPayload>>) => {
const commissionAmount = Number(payload.payload?.commission ?? '0');
const wageAmount = Number(payload.payload?.wages ?? '0');
const discountAmount = Number(payload.discount_amount ?? '0');
const profitAmount = Number(payload.payload?.profit ?? '0');
const unitPrice = Number(payload.unit_price ?? '0');
const quantity = Number(payload.quantity ?? '0');
const discountType = payload.payload?.discount_type;
const unitWithQuantity = unitPrice * quantity;
const totalAmountBeforeProfit = unitWithQuantity + wageAmount + commissionAmount;
const baseTotalAmount = totalAmountBeforeProfit + profitAmount;
const baseAmountForDiscountCalculation =
discountType === goldDiscountType.PROFIT ? profitAmount : unitWithQuantity;
const taxAmount =
(profitAmount +
commissionAmount +
wageAmount -
(discountType === goldDiscountType.PROFIT ? discountAmount : 0)) *
0.1;
const totalAmount = baseTotalAmount - discountAmount + taxAmount;
return {
unitWithQuantity,
totalAmountBeforeProfit,
baseTotalAmount,
taxAmount,
totalAmount,
baseAmountForDiscountCalculation,
};
};
@@ -42,17 +42,17 @@
<ng-template #labelSuffix> <ng-template #labelSuffix>
<p-selectButton <p-selectButton
[options]="discountTypeItems" [options]="discountTypeItems"
[(ngModel)]="discountType" [(ngModel)]="form.controls.payload.controls.discount_type.value"
[allowEmpty]="false" [allowEmpty]="false"
optionLabel="label" optionLabel="name"
optionValue="value" optionValue="id"
(onChange)="changeDiscountCalculation($event)" /> (onChange)="changeDiscountCalculation($event)" />
</ng-template> </ng-template>
</app-amount-percentage-input> </app-amount-percentage-input>
<hr /> <hr />
<div class="flex w-full flex-col justify-center gap-4"> <div class="flex w-full flex-col justify-center gap-4">
<pos-form-dialog-amount-card-template <shared-calculated-amount-card
[totalAmount]="totalAmount()" [totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()" [baseTotalAmount]="baseTotalAmount()"
[discountAmount]="form.controls.discount_amount.value || 0" [discountAmount]="form.controls.discount_amount.value || 0"
@@ -1,37 +1,49 @@
import { greaterThanValidator } from '@/core/validators/greater.validator'; import { greaterThanValidator } from '@/core/validators/greater.validator';
import { PosConfigGoldPriceService } from '@/domains/pos/modules/configs/components/goldPrice/services/main.service'; import { PosConfigGoldPriceService } from '@/domains/pos/modules/configs/components/goldPrice/services/main.service';
import { IPosOrderItem } from '@/domains/pos/modules/shop/models';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { EnumSelectComponent } from '@/shared/apiEnum/select.component'; import { EnumSelectComponent } from '@/shared/apiEnum/select.component';
import { InputComponent } from '@/shared/components'; import {
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component'; AmountPercentageInputComponent,
CalculatedAmountCardComponent,
InputComponent,
} from '@/shared/components';
import { LOCAL_ENUMS } from '@/shared/localEnum/constants';
import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType';
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat'; import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
import { formatWithCurrency } from '@/utils'; import { IGoldPayload } from '@/shared/models';
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core'; import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton'; import { SelectButton, SelectButtonChangeEvent } from 'primeng/selectbutton';
import { IGoldPayload, IPosOrderItem } from '../../../models'; import goldAmountCalculationUtil from './gold-amount-calculation.util';
import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-card-template.component';
@Component({ @Component({
selector: 'pos-gold-payload-form', selector: 'shared-gold-payload-form',
templateUrl: './form.component.html', templateUrl: './gold-form.component.html',
imports: [ imports: [
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
InputComponent, InputComponent,
EnumSelectComponent, EnumSelectComponent,
PosFormDialogAmountCardTemplateComponent,
SelectButton, SelectButton,
AmountPercentageInputComponent, AmountPercentageInputComponent,
ButtonDirective, ButtonDirective,
CalculatedAmountCardComponent,
], ],
}) })
export class PosGoldPayloadFormComponent extends AbstractForm< export class SharedGoldPayloadFormComponent extends AbstractForm<
IPosOrderItem<IGoldPayload>, IPosOrderItem<IGoldPayload>,
IPosOrderItem<IGoldPayload> IPosOrderItem<IGoldPayload>
> { > {
@Input({ required: true }) vatPercentage!: number;
@Input() isRapidInvoice: boolean = false;
@Input() ctaText: string = '';
@Output() onSubmitForm = new EventEmitter<IGoldPayload>();
@Output() onChangeTotalAmount = new EventEmitter<number>();
override defaultValues: Partial<IPosOrderItem<IGoldPayload>> = { override defaultValues: Partial<IPosOrderItem<IGoldPayload>> = {
// unit_price: 200_000_000, // unit_price: 200_000_000,
// quantity: 2, // quantity: 2,
@@ -43,26 +55,10 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
} as any, } as any,
}; };
@Input({ required: true }) vatPercentage!: number;
@Input() isRapidInvoice!: boolean;
@Output() onSubmitForm = new EventEmitter<IGoldPayload>();
@Output() onChangeTotalAmount = new EventEmitter<number>();
private readonly posGoldConfig = inject(PosConfigGoldPriceService); private readonly posGoldConfig = inject(PosConfigGoldPriceService);
readonly discountTypeItems = [ readonly discountTypeItems = LOCAL_ENUMS.goldDiscountTypes.items;
{
label: 'از سود',
value: 1,
},
{
label: 'از کل',
value: 2,
},
];
discountType = signal<number>(1);
unitWithQuantity = signal<number>(0); unitWithQuantity = signal<number>(0);
totalAmountBeforeProfit = signal<number>(0); totalAmountBeforeProfit = signal<number>(0);
baseTotalAmount = signal<number>(0); baseTotalAmount = signal<number>(0);
@@ -74,7 +70,11 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
goldDefaultUnitPrice = signal<number>(0); goldDefaultUnitPrice = signal<number>(0);
preparedCTAText = computed(() => preparedCTAText = computed(() =>
this.editMode ? 'اعمال ویرایش' : this.isRapidInvoice ? 'ثبت و پرداخت' : 'افزودن به لیست خرید' this.ctaText || this.editMode
? 'اعمال ویرایش'
: this.isRapidInvoice
? 'ثبت و پرداخت'
: 'افزودن به لیست خرید'
); );
private readonly initialForm = () => { private readonly initialForm = () => {
@@ -98,6 +98,7 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
profit: [this.initialValues?.payload?.profit || 0, [Validators.required]], profit: [this.initialValues?.payload?.profit || 0, [Validators.required]],
profit_percentage: [0, [Validators.required, Validators.max(100), Validators.min(0)]], profit_percentage: [0, [Validators.required, Validators.max(100), Validators.min(0)]],
karat: [this.initialValues?.payload?.karat || '', [Validators.required]], karat: [this.initialValues?.payload?.karat || '', [Validators.required]],
discount_type: [this.initialValues?.payload?.discount_type || goldDiscountType.PROFIT],
}), }),
}); });
@@ -133,31 +134,21 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
karat: this.form.controls.payload.controls.karat.value as TGoldKarat, karat: this.form.controls.payload.controls.karat.value as TGoldKarat,
profit: this.form.controls.payload.controls.profit.value || 0, profit: this.form.controls.payload.controls.profit.value || 0,
wages: this.form.controls.payload.controls.wages.value || 0, wages: this.form.controls.payload.controls.wages.value || 0,
discount_type:
this.form.controls.payload.controls.discount_type.value || goldDiscountType.PROFIT,
}, },
}); });
} }
updateCalculateAmount(payload: Partial<IPosOrderItem<any>>) { updateCalculateAmount(payload: Partial<IPosOrderItem<IGoldPayload>>) {
const commissionAmount = Number(payload.payload?.commission ?? '0'); const {
const wageAmount = Number(payload.payload?.wages ?? '0'); unitWithQuantity,
const discountAmount = Number(payload.discount_amount ?? '0'); totalAmountBeforeProfit,
const profitAmount = Number(payload.payload?.profit ?? '0'); baseTotalAmount,
const unitPrice = Number(payload.unit_price ?? '0'); taxAmount,
const quantity = Number(payload.quantity ?? '0'); totalAmount,
baseAmountForDiscountCalculation,
const unitWithQuantity = unitPrice * quantity; } = goldAmountCalculationUtil(payload);
const totalAmountBeforeProfit = unitWithQuantity + wageAmount + commissionAmount;
const baseTotalAmount = totalAmountBeforeProfit + profitAmount;
const baseAmountForDiscountCalculation =
this.discountType() === 1 ? profitAmount : unitWithQuantity;
const taxAmount =
(profitAmount +
commissionAmount +
wageAmount -
(this.discountType() === 1 ? discountAmount : 0)) *
0.1;
const totalAmount = baseTotalAmount - discountAmount + taxAmount;
this.unitWithQuantity.set(unitWithQuantity); this.unitWithQuantity.set(unitWithQuantity);
this.totalAmountBeforeProfit.set(totalAmountBeforeProfit); this.totalAmountBeforeProfit.set(totalAmountBeforeProfit);
@@ -169,16 +160,8 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
this.onChangeTotalAmount.emit(totalAmount); this.onChangeTotalAmount.emit(totalAmount);
} }
toAmountFormat(amount: number) {
if (!amount) {
return '';
}
return `(${formatWithCurrency(amount)})`;
}
changeDiscountCalculation($event: SelectButtonChangeEvent) { changeDiscountCalculation($event: SelectButtonChangeEvent) {
this.discountType.set($event.value); this.form.controls.payload.controls.discount_type.setValue($event.value);
this.updateCalculateAmount(this.form.value as any); this.updateCalculateAmount(this.form.value as any);
} }
} }
@@ -0,0 +1,3 @@
export * from './gold-amount-calculation.util';
export * from './gold-form.component';
export * from './standard-form.component';
@@ -19,7 +19,7 @@
<hr /> <hr />
<div class="flex w-full flex-col justify-center gap-4"> <div class="flex w-full flex-col justify-center gap-4">
<pos-form-dialog-amount-card-template <shared-calculated-amount-card
[totalAmount]="totalAmount()" [totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()" [baseTotalAmount]="baseTotalAmount()"
[discountAmount]="discountAmount()" [discountAmount]="discountAmount()"
@@ -1,34 +1,37 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { greaterThanValidator } from '@/core/validators'; import { greaterThanValidator } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import {
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component'; AmountPercentageInputComponent,
import { formatWithCurrency } from '@/utils'; CalculatedAmountCardComponent,
InputComponent,
} from '@/shared/components';
import { IStandardPayload } from '@/shared/models';
import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core'; import { Component, computed, EventEmitter, Input, Output, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, Validators } from '@angular/forms'; import { ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { IPosOrderItem, IStandardPayload } from '../../../models'; import { IPosOrderItem } from '../../../domains/pos/modules/shop/models';
import { PosFormDialogAmountCardTemplateComponent } from '../form-dialog-amount-card-template.component';
@Component({ @Component({
selector: 'pos-standard-payload-form', selector: 'shared-standard-payload-form',
templateUrl: './form.component.html', templateUrl: './standard-form.component.html',
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
InputComponent, InputComponent,
PosFormDialogAmountCardTemplateComponent,
AmountPercentageInputComponent, AmountPercentageInputComponent,
ButtonDirective, ButtonDirective,
CalculatedAmountCardComponent,
], ],
}) })
export class PosStandardPayloadFormComponent extends AbstractForm< export class SharedStandardPayloadFormComponent extends AbstractForm<
IPosOrderItem<IStandardPayload>, IPosOrderItem<IStandardPayload>,
IPosOrderItem<IStandardPayload> IPosOrderItem<IStandardPayload>
> { > {
@Input({ required: true }) measureUnit!: ISummary; @Input({ required: true }) measureUnit!: ISummary;
@Input({ required: true }) vatPercentage!: number; @Input({ required: true }) vatPercentage!: number;
@Input() isRapidInvoice!: boolean; @Input() isRapidInvoice: boolean = false;
@Input() ctaText: string = '';
@Output() onChangeTotalAmount = new EventEmitter<number>(); @Output() onChangeTotalAmount = new EventEmitter<number>();
private readonly initialForm = () => { private readonly initialForm = () => {
@@ -66,16 +69,13 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
taxAmount = signal<number>(0); taxAmount = signal<number>(0);
preparedCTAText = computed(() => preparedCTAText = computed(() =>
this.editMode ? 'اعمال ویرایش' : this.isRapidInvoice ? 'ثبت و پرداخت' : 'افزودن به لیست خرید' this.ctaText || this.editMode
? 'اعمال ویرایش'
: this.isRapidInvoice
? 'ثبت و پرداخت'
: 'افزودن به لیست خرید'
); );
toPriceFormat(amount: number) {
if (!amount) {
return '';
}
return `(${formatWithCurrency(amount)})`;
}
updateCalculateAmount(value: Partial<IPosOrderItem<IStandardPayload>>) { updateCalculateAmount(value: Partial<IPosOrderItem<IStandardPayload>>) {
const baseTotalAmount = (value.unit_price ?? 0) * (value.quantity ?? 0); const baseTotalAmount = (value.unit_price ?? 0) * (value.quantity ?? 0);
const discountAmount = Number(value.discount_amount ?? '0'); const discountAmount = Number(value.discount_amount ?? '0');
@@ -26,8 +26,8 @@
// showConfirm() { // showConfirm() {
// this.confirmService.confirm({ // this.confirmService.confirm({
// message: 'آیا پرداخت فاکتور انجام شده است؟', // message: 'آیا پرداخت صورت‌حساب‌ انجام شده است؟',
// header: 'وضعیت پرداخت فاکتور', // header: 'وضعیت پرداخت صورت‌حساب‌',
// acceptLabel: 'بله', // acceptLabel: 'بله',
// rejectLabel: 'خیر', // rejectLabel: 'خیر',
// accept: () => { // accept: () => {
@@ -35,8 +35,8 @@
// }, // },
// reject: () => { // reject: () => {
// this.toastService.info({ // this.toastService.info({
// text: 'می‌توانید از طریق منوی پرداخت فاکتور، در آینده اقدام به پرداخت نمایید.', // text: 'می‌توانید از طریق منوی پرداخت صورت‌حساب‌، در آینده اقدام به پرداخت نمایید.',
// title: 'پرداخت فاکتور انجام نشد', // title: 'پرداخت صورت‌حساب‌ انجام نشد',
// }); // });
// }, // },
// }); // });
@@ -1,10 +1,10 @@
import { IListConfig } from './list-config.model'; import { IListConfig } from './list-config.model';
export const saleInvoiceListConfig: IListConfig = { export const saleInvoiceListConfig: IListConfig = {
pageTitle: 'مدیریت فاکتورهای فروش', pageTitle: 'مدیریت صورت‌حساب‌های فروش',
addNewCtaLabel: 'افزودن فاکتور', addNewCtaLabel: 'افزودن صورت‌حساب‌',
emptyPlaceholderTitle: 'فاکتوری یافت نشد', emptyPlaceholderTitle: 'صورت‌حساب‌ی یافت نشد',
emptyPlaceholderDescription: 'برای افزودن فاکتور، روی دکمهٔ بالا کلیک کنید.', emptyPlaceholderDescription: 'برای افزودن صورت‌حساب‌، روی دکمهٔ بالا کلیک کنید.',
columns: [ columns: [
{ field: 'code', header: 'کد رهگیری' }, { field: 'code', header: 'کد رهگیری' },
{ {
@@ -27,7 +27,7 @@ export const saleInvoiceListConfig: IListConfig = {
}, },
{ {
field: 'invoice_date', field: 'invoice_date',
header: 'تاریخ فاکتور', header: 'تاریخ صورت‌حساب‌',
type: 'date', type: 'date',
}, },
{ {
@@ -0,0 +1,19 @@
import { ISelectItem } from '@/shared/abstractClasses';
export const goldDiscountType = {
PROFIT: 'PROFIT',
TOTAL: 'TOTAL',
} as const;
export type TGoldDiscountType = (typeof goldDiscountType)[keyof typeof goldDiscountType];
const translates: Record<TGoldDiscountType, string> = {
TOTAL: 'از کل',
PROFIT: 'از سود',
};
export const goldDiscountTypeSelect: ISelectItem[] = Object.keys(goldDiscountType).map((key) => ({
id: key,
name: translates[key as TGoldDiscountType],
}));
export const goldDiscountTypeLabel = 'نوع تخفیف';
@@ -1,3 +1,4 @@
import { goldDiscountTypeLabel, goldDiscountTypeSelect } from './goldDiscountType';
import { goldKaratLabel, goldKaratSelect } from './goldKarat'; import { goldKaratLabel, goldKaratSelect } from './goldKarat';
import { licenseStatusLabel, licenseStatusSelect } from './licenseStatus'; import { licenseStatusLabel, licenseStatusSelect } from './licenseStatus';
@@ -11,4 +12,9 @@ export const LOCAL_ENUMS = {
label: licenseStatusLabel, label: licenseStatusLabel,
items: licenseStatusSelect, items: licenseStatusSelect,
}, },
goldDiscountTypes: {
label: goldDiscountTypeLabel,
items: goldDiscountTypeSelect,
},
}; };
+3
View File
@@ -0,0 +1,3 @@
export * from './consumer.type';
export * from './enum_translate.type';
export * from './posPayments.type';
@@ -1,6 +1,8 @@
import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat'; import { TGoldKarat } from '@/shared/localEnum/constants/goldKarat';
import { TGoldDiscountType } from '../localEnum/constants/goldDiscountType';
export interface IGoldPayload { export interface IGoldPayload {
discount_type: TGoldDiscountType;
karat: TGoldKarat; karat: TGoldKarat;
wages: number; wages: number;
profit: number; profit: number;
@@ -1,7 +1,58 @@
<div #wrapper [attr.data-name]="name" class="flatWrapper w-full"> <uikit-field [label]="label!" [name]="name!" [alignment]="alignment" class="w-full" (click)="op.toggle($event)">
<input data-input [attr.placeholder]="placeholder" [name]="name" class="flatpicker-field w-full" /> <input pInputText readonly [name]="name" [value]="valueToShow" class="w-full" />
</uikit-field>
<p-popover #op appendTo="body">
<div #wrapperRef class="dir-rtl border-divider bg-background-paper w-full min-w-sm overflow-auto rounded-md">
<div class="bg-primary-main sticky top-0 flex justify-center px-3">
<div class="flex items-center gap-3">
<div class="flex h-8 items-center">
<div class="px-2">
<i class="pi pi-chevron-right cursor-pointer" (click)="changeMonth(-1)"></i>
</div>
@if (hint) { <div class="flex w-36 items-center justify-center px-3 text-lg font-bold select-none">
<small [attr.id]="name + '-help'">{{ hint }}</small> {{ monthInfo().monthTitle }}
</div>
<div class="px-2">
<i class="pi pi-chevron-left cursor-pointer" (click)="changeMonth(1)"></i>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-7">
@for (dayName of daysName; track $index) {
<div class="text-muted-color flex items-center justify-center py-2">
{{ dayName[0] }}
</div>
} }
</div> </div>
<hr />
<div class="relative grow" style="direction: rtl">
@for (weekDays of weekDaysDates(); track $index) {
<div class="grid grid-cols-7">
@for (day of weekDays; track $index) {
<div
class="relative flex aspect-square items-center justify-center p-1"
[class.bg-background-default]="closedDays.includes($index)">
@if (!(day.isNextMonth || day.isPastMonth)) {
<span
class="flex aspect-square w-full cursor-pointer items-center justify-center rounded-lg px-2 py-1 text-lg transition-colors select-none"
[ngClass]="{
'bg-surface-ground font-bold': day.isCurrentDay,
'bg-primary! text-primary-contrast!': isSelected(day),
'text-muted-color! font-normal': day.isDisabled,
}"
(click)="selectDate(day)">
{{ day.day }}
</span>
}
</div>
}
</div>
}
</div>
</div>
</p-popover>
+130 -48
View File
@@ -1,75 +1,157 @@
import { Maybe } from '@/core';
import { nowJalali } from '@/utils';
import { CommonModule } from '@angular/common';
import { import {
AfterViewInit,
Component, Component,
ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnDestroy,
Output, Output,
ViewChild, ViewChild,
computed,
effect,
signal,
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { FormControl } from '@angular/forms';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import flatpickr from 'flatpickr-wrap'; import jalaliday from 'jalaliday';
import { Persian } from 'flatpickr-wrap/dist/l10n/fa';
import { BaseOptions, DateOption } from 'flatpickr-wrap/dist/types/options'; import { InputText } from 'primeng/inputtext';
import { InputTextModule } from 'primeng/inputtext'; import { Popover } from 'primeng/popover';
import { Maybe } from '@/core';
import { GREGORIAN_DATE_FORMATS, formatDate, formatJalali } from '@/utils';
import { UikitFieldComponent } from '../uikit-field.component'; import { UikitFieldComponent } from '../uikit-field.component';
import {
MonthInfo,
MonthlyWeekDate,
daysName,
prepareMonthInfo,
prepareWeekDayDates,
} from './monthly-calendar.helper.util';
dayjs.extend(jalaliday);
@Component({ @Component({
selector: 'uikit-datepicker', selector: 'app-datepicker',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, InputTextModule, UikitFieldComponent],
templateUrl: './datepicker.component.html', templateUrl: './datepicker.component.html',
imports: [CommonModule, Popover, InputText, UikitFieldComponent],
}) })
export class UikitFlatpickrJalaliComponent implements AfterViewInit, OnDestroy { export class DatepickerComponent {
@Input() control?: FormControl<Maybe<string>>; @Input({ required: true }) control!: FormControl<Maybe<string>>;
@Input() placeholder = 'انتخاب تاریخ';
@Input() alignment: 'vertical' | 'horizontal' = 'vertical';
@Input() name = 'date';
@Input() label = 'تاریخ'; @Input() label = 'تاریخ';
@Input() disabled = false;
@Input() name: string = 'datepicker'; @Input() closeOnSelect = true;
@Input() options: Partial<BaseOptions> = {};
@Input() showLabel: boolean = true; /**
@Input() showErrors: boolean = true; * Gregorian YYYY-MM-DD
@Input() hint?: string; */
@Input() minDate?: DateOption; @Input() min?: string;
@Input() maxDate?: DateOption;
/**
* Gregorian YYYY-MM-DD
*/
@Input() max?: string;
@Input() closedDays: number[] = [];
@Output() valueChange = new EventEmitter<string>(); @Output() valueChange = new EventEmitter<string>();
@ViewChild('wrapper', { static: true }) wrapperRef!: ElementRef<HTMLElement>; @Output() clickOnDayEvent = new EventEmitter<MonthlyWeekDate>();
private fpInstance: any | null = null; @ViewChild('op') op!: Popover;
ngAfterViewInit(): void { readonly daysName = daysName;
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
wrap: true,
...this.options,
locale: Persian,
disableMobile: true,
clickOpens: !this.disabled,
minDate: this.minDate,
maxDate: this.maxDate,
now: nowJalali(),
altInputClass: 'flatpicker-field w-full',
altInput: true,
dateFormat: 'Y-m-d',
altFormat: 'Y/m/d',
onChange: (selectedDates: Date[], dateStr: string) => { /**
this.valueChange.emit(dateStr); * Currently displayed month
if (this.control) this.control.setValue(dayjs(dateStr).toISOString()); */
}, private readonly activeDateSignal = signal(dayjs().format('YYYY-MM-DD'));
monthInfo = computed<MonthInfo>(() => prepareMonthInfo(this.activeDateSignal()));
weekDaysDates = computed(() => prepareWeekDayDates(this.monthInfo(), this.min, this.max));
constructor() {
effect(() => {
const value = this.value;
if (value) {
this.activeDateSignal.set(value);
}
}); });
} }
ngOnDestroy(): void { ngOnInit(): void {
if (this.fpInstance?.destroy) { if (this.control?.value) {
this.fpInstance.destroy(); this.activeDateSignal.set(this.control.value);
this.fpInstance = null;
} }
this.control.valueChanges.subscribe((value) => {
if (value) {
this.activeDateSignal.set(value);
}
});
}
get value(): Maybe<string> {
return this.control.value
? formatDate(this.control.value, 'gregory', 'en', GREGORIAN_DATE_FORMATS.NUMERIC)
: null;
}
get valueToShow(): Maybe<string> {
if (!this.value) {
return null;
}
return formatJalali(this.value);
}
get activeDate(): string {
return this.activeDateSignal();
}
set activeDate(date: string) {
this.activeDateSignal.set(date);
}
changeMonth(months: number): void {
this.activeDateSignal.set(
dayjs(this.activeDateSignal()).add(months, 'month').format('YYYY-MM-DD')
);
}
selectDate(day: MonthlyWeekDate): void {
if (day.isDisabled) {
return;
}
this.control.setValue(day.gregorianDate);
this.control.markAsDirty();
this.control.markAsTouched();
this.valueChange.emit(day.gregorianDate);
this.clickOnDayEvent.emit(day);
if (this.closeOnSelect) {
this.op.hide();
}
}
isSelected(day: MonthlyWeekDate): boolean {
return this.value === day.gregorianDate;
}
trackByIndex(index: number): number {
return index;
} }
} }
@@ -0,0 +1,162 @@
import dayjs, { Dayjs } from 'dayjs';
import jalaliday from 'jalaliday';
dayjs.extend(jalaliday);
export interface MonthInfo {
isCurrentMonth: boolean;
firstDateOfMonth: dayjs.Dayjs;
lastDateOfMonth: dayjs.Dayjs;
monthDaysCount: number;
firstDayOfMonth: number;
jalaliFirstDayOfMonth: number;
monthTitle: string;
monthIndex: number;
prevMonthIsDisabled: boolean;
nextMonthIsDisabled: boolean;
isDisabled: boolean;
}
export interface MonthlyWeekDate {
gregorianDate: string;
date: string;
isNextMonth: boolean;
isPastMonth: boolean;
isCurrentDay: boolean;
isPastDate: boolean;
isFutureDate: boolean;
dayText: string;
day: number;
isHoliday: boolean;
isDisabled: boolean;
}
export const daysName = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'];
export function prepareMonthInfo(activeDate: string | Date, min?: string, max?: string): MonthInfo {
const date = dayjs(activeDate).calendar('jalali');
const firstDayOfMonth = date.startOf('month').day();
return {
isCurrentMonth: dayjs().calendar('jalali').isSame(date, 'month'),
firstDateOfMonth: date.startOf('month'),
lastDateOfMonth: date.endOf('month'),
monthDaysCount: date.daysInMonth(),
firstDayOfMonth,
jalaliFirstDayOfMonth: firstDayOfMonth === 6 ? 0 : firstDayOfMonth + 1,
monthTitle: date.locale('fa').format('MMMM YYYY'),
monthIndex: date.get('month'),
isDisabled: false,
nextMonthIsDisabled: false,
prevMonthIsDisabled: false,
};
}
export function prepareWeekDayDates(
monthInfo: MonthInfo,
min?: string,
max?: string
): MonthlyWeekDate[][] {
const weekDaysDates: MonthlyWeekDate[][] = [];
const { monthDaysCount, firstDateOfMonth } = monthInfo;
for (let i = 0; i < monthDaysCount; i++) {
const date = firstDateOfMonth.add(i, 'day');
const dateDay = date.day();
const persianDateDay = dateDay === 6 ? 0 : dateDay + 1;
if (i === 0 || persianDateDay === 0) {
weekDaysDates.push(new Array(7) as MonthlyWeekDate[]);
}
if (i === 0) {
for (let j = 0; j < persianDateDay; j++) {
weekDaysDates[0][j] = weekDateFactory(
date.add(-persianDateDay + j, 'day'),
monthInfo,
min,
max
);
}
}
weekDaysDates[weekDaysDates.length - 1][persianDateDay] = weekDateFactory(
date,
monthInfo,
min,
max
);
if (i === monthDaysCount - 1) {
for (let j = persianDateDay + 1; j < 7; j++) {
weekDaysDates[weekDaysDates.length - 1][j] = weekDateFactory(
date.add(j - persianDateDay, 'day'),
monthInfo,
min,
max
);
}
}
}
return weekDaysDates;
}
function weekDateFactory(
date: dayjs.Dayjs,
monthInfo: MonthInfo,
min?: string,
max?: string
): MonthlyWeekDate {
const now = dayjs().calendar('jalali');
const formattedDate = date.format('YYYY-MM-DD');
return {
gregorianDate: date.calendar('gregory').format('YYYY-MM-DD'),
date: formattedDate,
isNextMonth: date.month() > monthInfo.monthIndex,
isPastMonth: date.month() < monthInfo.monthIndex,
isCurrentDay: now.isSame(date, 'day'),
isPastDate: date.isBefore(now, 'day'),
isFutureDate: now.isBefore(date, 'day'),
dayText: date.locale('fa').format('D MMMM'),
day: Number(date.locale('fa').format('D')),
isDisabled: isDisabledDate(date, min, max),
// Replace with your own holiday service
isHoliday: false,
};
}
function isDisabledDate(date: Dayjs, min?: string, max?: string) {
if (min && date.isBefore(dayjs(min), 'day')) {
return true;
}
if (max && date.isAfter(dayjs(max), 'day')) {
return true;
}
return false;
}
+23 -7
View File
@@ -1,4 +1,4 @@
import { Component, computed, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output, signal, SimpleChanges } from '@angular/core';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
@Component({ @Component({
@@ -13,13 +13,16 @@ export class PaginatorComponent {
@Input() loading: boolean = false; @Input() loading: boolean = false;
@Output() onChange = new EventEmitter<number>(); @Output() onChange = new EventEmitter<number>();
pagesToShow = computed(() => { pagesToShow = signal<number[]>([]);
init() {
const maxVisible = 5; const maxVisible = 5;
if (this.totalPages <= maxVisible) { let pagesToShow = [];
return Array.from({ length: this.totalPages }, (_, i) => i + 1);
}
if (this.totalPages <= maxVisible) {
pagesToShow = Array.from({ length: this.totalPages }, (_, i) => i + 1);
} else {
let start = Math.max(1, this.currentPage - Math.floor(maxVisible / 2)); let start = Math.max(1, this.currentPage - Math.floor(maxVisible / 2));
let end = start + maxVisible - 1; let end = start + maxVisible - 1;
@@ -29,8 +32,11 @@ export class PaginatorComponent {
start = end - maxVisible + 1; start = end - maxVisible + 1;
} }
return Array.from({ length: end - start + 1 }, (_, i) => start + i); pagesToShow = Array.from({ length: end - start + 1 }, (_, i) => start + i);
}); }
this.pagesToShow.set(pagesToShow);
}
onPageChange(newPage: number) { onPageChange(newPage: number) {
this.onChange.emit(newPage); this.onChange.emit(newPage);
@@ -46,4 +52,14 @@ export class PaginatorComponent {
this.onPageChange(this.currentPage + 1); this.onPageChange(this.currentPage + 1);
} }
} }
ngOnInit() {
this.init();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['totalPages'] || changes['currentPage']) {
this.init();
}
}
} }
+1 -1
View File
@@ -2,7 +2,7 @@
<ng-template #fieldTemplate> <ng-template #fieldTemplate>
@if (showLabel) { @if (showLabel) {
<div [class]="`flex gap-1 ${alignment === 'horizontal' ? 'items-center' : 'flex-col'} ${className || ''}`"> <div [class]="`flex shrink-0 gap-1 ${alignment === 'horizontal' ? 'items-center' : 'flex-col'} ${className || ''}`">
@if (labelView) { @if (labelView) {
<ng-container [ngTemplateOutlet]="labelView"> </ng-container> <ng-container [ngTemplateOutlet]="labelView"> </ng-container>
} @else { } @else {
+31 -19
View File
@@ -1,7 +1,9 @@
import jalaliPlugin from '@zoomit/dayjs-jalali-plugin';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/fa'; import 'dayjs/locale/fa';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import jalaliPlugin from 'jalaliday';
type dateInput = string | Date | Dayjs | number;
// Extend dayjs with plugins // Extend dayjs with plugins
dayjs.extend(jalaliPlugin); dayjs.extend(jalaliPlugin);
@@ -80,7 +82,7 @@ export const GREGORIAN_DATE_FORMATS = {
* formatJalali(new Date(), JALALI_DATE_FORMATS.FULL_WITH_WEEKDAY) // 'پنج‌شنبه، ۱۳ شهریور ۱۳۹۷' * formatJalali(new Date(), JALALI_DATE_FORMATS.FULL_WITH_WEEKDAY) // 'پنج‌شنبه، ۱۳ شهریور ۱۳۹۷'
*/ */
export function formatJalali( export function formatJalali(
date: string | Date | Dayjs | number, date: dateInput,
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);
@@ -98,7 +100,7 @@ export function formatJalali(
* formatGregorian(new Date(), GREGORIAN_DATE_FORMATS.NUMERIC_WITH_TIME) // '2018-09-04 16:30' * formatGregorian(new Date(), GREGORIAN_DATE_FORMATS.NUMERIC_WITH_TIME) // '2018-09-04 16:30'
*/ */
export function formatGregorian( export function formatGregorian(
date: string | Date | Dayjs | number, date: dateInput,
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);
@@ -115,7 +117,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 | Date | Dayjs | number, format?: string): Dayjs { export function parseJalali(jalaliDate: dateInput, format?: string): Dayjs {
return dayjs(jalaliDate, { jalali: true, ...(format && { format }) }); return dayjs(jalaliDate, { jalali: true, ...(format && { format }) });
} }
@@ -192,7 +194,7 @@ export function nowGregorian(format: string = GREGORIAN_DATE_FORMATS.NUMERIC_WIT
* @example * @example
* relativeTimeJalali(dayjs().subtract(2, 'day')) // '۲ روز پیش' * relativeTimeJalali(dayjs().subtract(2, 'day')) // '۲ روز پیش'
*/ */
export function relativeTimeJalali(date: string | Date | Dayjs | number): string { export function relativeTimeJalali(date: dateInput): string {
return dayjs(date).locale('fa').fromNow(); return dayjs(date).locale('fa').fromNow();
} }
@@ -219,7 +221,7 @@ export function isValidJalaliDate(jalaliDate: string): boolean {
* @example * @example
* startOfDayJalali('1398-10-17') // Dayjs instance at 00:00:00 * startOfDayJalali('1398-10-17') // Dayjs instance at 00:00:00
*/ */
export function startOfDayJalali(date: string | Date | Dayjs | number): Dayjs { export function startOfDayJalali(date: dateInput): Dayjs {
return dayjs(date).calendar('jalali').startOf('day'); return dayjs(date).calendar('jalali').startOf('day');
} }
@@ -232,7 +234,7 @@ export function startOfDayJalali(date: string | Date | Dayjs | number): Dayjs {
* @example * @example
* endOfDayJalali('1398-10-17') // Dayjs instance at 23:59:59 * endOfDayJalali('1398-10-17') // Dayjs instance at 23:59:59
*/ */
export function endOfDayJalali(date: string | Date | Dayjs | number): Dayjs { export function endOfDayJalali(date: dateInput): Dayjs {
return dayjs(date).calendar('jalali').endOf('day'); return dayjs(date).calendar('jalali').endOf('day');
} }
@@ -250,7 +252,7 @@ export function endOfDayJalali(date: string | Date | Dayjs | number): Dayjs {
* formatDate('2018-09-04', 'gregory', 'en', 'MMMM DD YYYY') // 'September 04 2018' * formatDate('2018-09-04', 'gregory', 'en', 'MMMM DD YYYY') // 'September 04 2018'
*/ */
export function formatDate( export function formatDate(
date: string | Date | Dayjs | number, date: dateInput,
calendar: 'jalali' | 'gregory', calendar: 'jalali' | 'gregory',
locale: 'fa' | 'en', locale: 'fa' | 'en',
format: string format: string
@@ -302,28 +304,38 @@ export function getJalaliWeekdayName(dayNumber: number): string {
} }
export function jalaliDiff( export function jalaliDiff(
date1: string | number | Date | Dayjs, date1: dateInput,
date2: string | number | Date | Dayjs, date2: dateInput,
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 jalaliAddUnit(
date1: string | number | Date | Dayjs, date: dateInput,
date2: string | number | Date | Dayjs count: number,
): boolean { unit: dayjs.ManipulateType = 'days'
): Dayjs {
return dayjs(date, { jalali: true }).add(count, unit);
}
export function gregorianAddUnit(
date: dateInput,
count: number,
unit: dayjs.ManipulateType = 'days'
): Dayjs {
return dayjs(date).add(count, unit);
}
export function isJalaliBefore(date1: dateInput, date2: dateInput): boolean {
return toJalali(date1).isBefore(toJalali(date2)); return toJalali(date1).isBefore(toJalali(date2));
} }
export function isJalaliAfter( export function isJalaliAfter(date1: dateInput, date2: dateInput): boolean {
date1: string | number | Date | Dayjs,
date2: string | number | Date | Dayjs
): boolean {
return toJalali(date1).isAfter(toJalali(date2)); return toJalali(date1).isAfter(toJalali(date2));
} }
export function toJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): Dayjs { export function toJalali(date: dateInput, format = 'YYYY/MM/DD'): Dayjs {
return dayjs(date).calendar('jalali'); return dayjs(date).calendar('jalali');
} }
-282
View File
@@ -1,282 +0,0 @@
.flatpickr-calendar {
direction: rtl;
background: var(--color-surface-ground);
}
.flatpickr-time {
direction: ltr;
}
.flat {
display: flex;
}
.flatpickr-calendar .hasWeeks .dayContainer {
/*border-left: 0; */
}
.flatpickr-calendar:before,
.flatpickr-calendar:after {
/*left: 22px; */
}
.flatpickr-calendar.rightMost:before,
.flatpickr-calendar.arrowRight:before,
.flatpickr-calendar.rightMost:after,
.flatpickr-calendar.arrowRight:after {
/*left: auto;*/
/*right: 22px;*/
}
.flatpickr-calendar.arrowTop:before {
/*border-bottom-color: #e6e6e6;*/
}
.flatpickr-calendar.arrowTop:after {
/*border-bottom-color: #fff;*/
}
.flatpickr-calendar.arrowBottom:before {
/*border-top-color: #e6e6e6;*/
}
.flatpickr-calendar.arrowBottom:after {
/*border-top-color: #fff;*/
}
.flatpickr-months {
display: flex;
align-items: center;
}
.flatpickr-months .flatpickr-month {
color: var(--p-text-muted-color);
fill: var(--p-text-muted-color);
height: 40px;
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
color: var(--p-text-muted-color);
fill: var(--p-text-muted-color);
position: relative !important;
rotate: 180deg;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
right: 0;
left: auto;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
left: 0;
right: auto;
}
.flatpickr-months .flatpickr-prev-month:hover,
.flatpickr-months .flatpickr-next-month:hover {
/*color: #959ea9;*/
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
/*fill: #f64747;*/
}
.numInputWrapper span {
right: auto;
left: 0;
/*border: 1px solid rgba(57,57,57,0.15);*/
}
.numInputWrapper span:hover {
/*background: rgba(0,0,0,0.1);*/
}
.numInputWrapper span:active {
/*background: rgba(0,0,0,0.2);*/
}
.numInputWrapper span.arrowUp:after {
/*border-bottom: 4px solid rgba(57,57,57,0.6);*/
}
.numInputWrapper span.arrowDown:after {
/*border-top: 4px solid rgba(57,57,57,0.6);*/
}
.numInputWrapper span svg path {
/*fill: rgba(0,0,0,0.5);*/
}
.numInputWrapper:hover {
/*background: rgba(0,0,0,0.05);*/
}
.flatpickr-current-month {
padding: 0;
position: relative;
left: auto;
right: auto;
display: flex;
margin: auto;
gap: 8px;
height: 100%;
}
.flatpickr-current-month > * {
flex: 1;
}
.flatpickr-current-month span.cur-month {
/*margin-left: 0.5ch;*/
}
.flatpickr-current-month span.cur-month:hover {
/*background: rgba(0,0,0,0.05);*/
}
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
border-bottom-color: var(--p-text-color);
}
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
border-top-color: var(--p-text-color);
}
.flatpickr-current-month input.cur-year {
font-size: 16px;
display: flex;
align-items: center;
height: 100%;
padding: 0 0 0 20px;
}
.flatpickr-current-month input.cur-year[disabled],
.flatpickr-current-month input.cur-year[disabled]:hover {
/*color: rgba(0,0,0,0.5);*/
}
.flatpickr-current-month .flatpickr-monthDropdown-months {
font-size: 16px;
height: 100%;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:hover {
/*background: rgba(0,0,0,0.05);*/
}
span.flatpickr-weekday {
color: var(--p-text-muted-color);
}
.dayContainer {
text-align: left;
}
/*.dayContainer + .dayContainer {*/
/* -webkit-box-shadow: -1px 0 0 #e6e6e6;*/
/* box-shadow: -1px 0 0 #e6e6e6;*/
/*}*/
.flatpickr-day {
color: var(--p-text-color);
}
.flatpickr-day.inRange,
.flatpickr-day.prevMonthDay.inRange,
.flatpickr-day.nextMonthDay.inRange,
.flatpickr-day.today.inRange,
.flatpickr-day.prevMonthDay.today.inRange,
.flatpickr-day.nextMonthDay.today.inRange,
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover,
.flatpickr-day:focus,
.flatpickr-day.prevMonthDay:focus,
.flatpickr-day.nextMonthDay:focus {
background: var(--p-blue-300);
border-color: var(--focus-ring-color);
}
.flatpickr-day.today {
/*border-color: #959ea9;*/
}
.flatpickr-day.today:hover,
.flatpickr-day.today:focus {
/*border-color: #959ea9;*/
/*background: #959ea9;*/
/*color: #fff;*/
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected.inRange,
.flatpickr-day.startRange.inRange,
.flatpickr-day.endRange.inRange,
.flatpickr-day.selected:focus,
.flatpickr-day.startRange:focus,
.flatpickr-day.endRange:focus,
.flatpickr-day.selected:hover,
.flatpickr-day.startRange:hover,
.flatpickr-day.endRange:hover,
.flatpickr-day.selected.prevMonthDay,
.flatpickr-day.startRange.prevMonthDay,
.flatpickr-day.endRange.prevMonthDay,
.flatpickr-day.selected.nextMonthDay,
.flatpickr-day.startRange.nextMonthDay,
.flatpickr-day.endRange.nextMonthDay {
background: var(--color-primary);
color: #fff;
border-color: var(--focus-ring-color);
}
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
/*-webkit-box-shadow: -10px 0 0 #569ff7;*/
/*box-shadow: -10px 0 0 #569ff7;*/
}
.flatpickr-day.inRange {
/*-webkit-box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;*/
/* box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;*/
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay,
.flatpickr-day.notAllowed,
.flatpickr-day.notAllowed.prevMonthDay,
.flatpickr-day.notAllowed.nextMonthDay {
color: var(--p-text-color);
opacity: 0.4;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover {
/*color: rgba(57,57,57,0.1);*/
}
.flatpickr-day.week.selected {
/*-webkit-box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;*/
/* box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;*/
}
.flatpickr-weekwrapper {
/*float: left;*/
}
.flatpickr-weekwrapper .flatpickr-weeks {
/*-webkit-box-shadow: 1px 0 0 #e6e6e6;*/
/* box-shadow: 1px 0 0 #e6e6e6;*/
}
.flatpickr-weekwrapper span.flatpickr-day,
.flatpickr-weekwrapper span.flatpickr-day:hover {
/*color: rgba(57,57,57,0.3);*/
}
.flatpickr-time .numInputWrapper {
/*float: left;*/
}
.flatpickr-time .numInputWrapper span.arrowUp:after {
border-bottom-color: var(--p-text-color);
}
.flatpickr-time .numInputWrapper span.arrowDown:after {
border-top-color: var(--p-text-color);
}
.flatpickr-time input {
color: var(--p-text-color);
}
.flatpickr-time .flatpickr-time-separator,
.flatpickr-time .flatpickr-am-pm {
/*float: left;*/
color: var(--p-text-color);
}
.flatpickr-time input:hover,
.flatpickr-time .flatpickr-am-pm:hover,
.flatpickr-time input:focus,
.flatpickr-time .flatpickr-am-pm:focus {
background: var(--color-surface-hover);
}
-3
View File
@@ -1,8 +1,5 @@
/* 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/flatpickr.css";
@use "./flatpicker.css";
@use "./layout/layout.scss"; @use "./layout/layout.scss";
@use "primeicons/primeicons.css"; @use "primeicons/primeicons.css";
@use "./rtlSupport.scss"; @use "./rtlSupport.scss";
+2 -2
View File
@@ -1,8 +1,8 @@
// TIS tenant environment configuration // TIS tenant environment configuration
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: 'https://psp-api.shift-am.ir', // apiBaseUrl: 'https://psp-api.shift-am.ir',
// apiBaseUrl: 'http://192.168.128.73:5002', apiBaseUrl: 'http://192.168.128.73:5002',
// apiBaseUrl: 'http://localhost:5002', // apiBaseUrl: 'http://localhost:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,

Some files were not shown because too many files have changed in this diff Show More