Compare commits

...

4 Commits

Author SHA1 Message Date
ahasani 88f45eee38 refactor: update labels and translations in various components
Production CI / validate-and-build (push) Failing after 1m2s
- Changed 'تغییر گذرواژه' to 'تغییر رمز عبور' in layout.component.ts
- Updated HTML structure in list.component.html and saleInvoices components for consistency and improved styling.
- Modified sale-invoice-card.component.html to enhance the display of invoice details.
- Adjusted sale-invoice-card.component.ts to handle customer types more effectively.
- Enhanced the handling of invoice statuses in saleInvoices components.
- Improved layout and styling in shop components for better user experience.
- Refactored key-value component for better readability and maintainability.
- Updated upload file component to streamline file selection process.
- Added new styles in customize.scss and psp.scss for consistent UI.
- Adjusted environment configuration for better API endpoint management.
2026-06-09 13:31:48 +03:30
ahasani 5fa07c7ee8 feat: enhance invoice correction form with item editing and price info display 2026-06-08 13:35:58 +03:30
ahasani 788f4023f3 refactor: remove change password form and related service, update root component to use tabs 2026-06-07 14:33:11 +03:30
ahasani 2f67801700 feat: implement change password functionality with dialog and service integration 2026-06-07 13:56:36 +03:30
57 changed files with 725 additions and 571 deletions
+8
View File
@@ -100,6 +100,10 @@
"input": "public-tis" "input": "public-tis"
} }
], ],
"styles": [
"src/assets/styles.scss",
"src/assets/psp.scss"
],
"budgets": [ "budgets": [
{ {
"maximumError": "3MB", "maximumError": "3MB",
@@ -144,6 +148,10 @@
"input": "public-tis" "input": "public-tis"
} }
], ],
"styles": [
"src/assets/styles.scss",
"src/assets/psp.scss"
],
"budgets": [ "budgets": [
{ {
"maximumError": "2.5MB", "maximumError": "2.5MB",
@@ -1,16 +1,14 @@
<shared-dialog <shared-dialog
header="ویرایش گذرواژه" header="تغییر رمز عبور"
[(visible)]="visible" [(visible)]="visible"
[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">
<shared-password-input <shared-password-input
[passwordControl]="form.controls.password" [passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" [confirmPasswordControl]="form.controls.confirmPassword" />
/>
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
pageTitle="لیست صورت‌حساب‌ها" pageTitle="صورت‌حساب‌ها"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="تا به حال صورت‌حسابی توسط این پایانه ایجاد نشده است." emptyPlaceholderTitle="تا به حال صورت‌حسابی توسط این پایانه ایجاد نشده است."
[items]="items()" [items]="items()"
@@ -1,11 +1,10 @@
<app-card-data cardTitle="تغییر گذرواژه" [editable]="false"> <app-card-data cardTitle="تغییر رمز عبور" [editable]="false">
<form [formGroup]="form" (submit)="submit()" class="max-w-lg mx-auto"> <form [formGroup]="form" (submit)="submit()" class="mx-auto max-w-lg">
<shared-password-input <shared-password-input
[passwordControl]="form.controls.password" [passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" [confirmPasswordControl]="form.controls.confirmPassword" />
/> <button type="submit" pButton [disabled]="form.invalid || loading()" class="mx-auto w-full max-w-xs">
<button type="submit" pButton [disabled]="form.invalid || loading()" class="w-full max-w-xs mx-auto"> تغییر رمز عبور
تغییر گذرواژه
</button> </button>
</form> </form>
</app-card-data> </app-card-data>
@@ -25,7 +25,7 @@ export class ConsumerResetPasswordCardComponent {
password: fieldControl.password(), password: fieldControl.password(),
confirmPassword: fieldControl.confirmPassword(), confirmPassword: fieldControl.confirmPassword(),
}, },
{ validators: [MustMatch('password', 'confirmPassword')] }, { validators: [MustMatch('password', 'confirmPassword')] }
); );
submit() { submit() {
@@ -40,7 +40,7 @@ export class ConsumerResetPasswordCardComponent {
.pipe(finalize(() => this.loading.set(false))) .pipe(finalize(() => this.loading.set(false)))
.subscribe(() => { .subscribe(() => {
this.form.reset(); this.form.reset();
this.toastService.success({ text: 'گذرواژه با موفقیت به‌روز شد.' }); this.toastService.success({ text: 'رمز عبور با موفقیت به‌روز شد.' });
}); });
} }
} }
@@ -1,5 +1,5 @@
<app-page-data-list <app-page-data-list
[pageTitle]="'لیست صورت‌حساب‌ها'" [pageTitle]="'صورت‌حساب‌ها'"
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="صورت‌حساب‌ی یافت نشد" emptyPlaceholderTitle="صورت‌حساب‌ی یافت نشد"
[items]="items()" [items]="items()"
@@ -1,16 +1,14 @@
<shared-dialog <shared-dialog
header="ویرایش گذرواژه" header="تغییر رمز عبور"
[(visible)]="visible" [(visible)]="visible"
[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">
<shared-password-input <shared-password-input
[passwordControl]="form.controls.password" [passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" [confirmPasswordControl]="form.controls.confirmPassword" />
/>
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="submitLoading()" (onCancel)="close()" />
</form> </form>
@@ -1,11 +1,10 @@
<app-card-data cardTitle="تغییر گذرواژه" [editable]="false"> <app-card-data cardTitle="تغییر رمز عبور" [editable]="false">
<form [formGroup]="form" (submit)="submit()" class="max-w-lg mx-auto"> <form [formGroup]="form" (submit)="submit()" class="mx-auto max-w-lg">
<shared-password-input <shared-password-input
[passwordControl]="form.controls.password" [passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" [confirmPasswordControl]="form.controls.confirmPassword" />
/> <button type="submit" pButton [disabled]="form.invalid || loading()" class="mx-auto w-full max-w-xs">
<button type="submit" pButton [disabled]="form.invalid || loading()" class="w-full max-w-xs mx-auto"> تغییر رمز عبور
تغییر گذرواژه
</button> </button>
</form> </form>
</app-card-data> </app-card-data>
@@ -25,7 +25,7 @@ export class PartnerResetPasswordCardComponent {
password: fieldControl.password(), password: fieldControl.password(),
confirmPassword: fieldControl.confirmPassword(), confirmPassword: fieldControl.confirmPassword(),
}, },
{ validators: [MustMatch('password', 'confirmPassword')] }, { validators: [MustMatch('password', 'confirmPassword')] }
); );
submit() { submit() {
@@ -40,7 +40,7 @@ export class PartnerResetPasswordCardComponent {
.pipe(finalize(() => this.loading.set(false))) .pipe(finalize(() => this.loading.set(false)))
.subscribe(() => { .subscribe(() => {
this.form.reset(); this.form.reset();
this.toastService.success({ text: 'گذرواژه با موفقیت به‌روز شد.' }); this.toastService.success({ text: 'رمز عبور با موفقیت به‌روز شد.' });
}); });
} }
} }
@@ -0,0 +1 @@
<shared-change-password-form-dialog [(visible)]="visible" [loading]="submitLoading()" (onSubmit)="submit($event)" />
@@ -0,0 +1,36 @@
import { ToastService } from '@/core/services/toast.service';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import {
ChangePasswordFormDialogComponent,
IChangePasswordSubmitPayload,
} from '@/shared/components';
import { Component, inject, signal } from '@angular/core';
import { finalize } from 'rxjs';
import { PosChangePasswordService } from '../services/password';
@Component({
selector: 'pos-change-password',
templateUrl: 'change-password.component.html',
imports: [ChangePasswordFormDialogComponent],
})
export class PosChangePasswordComponent extends AbstractDialog {
private readonly service = inject(PosChangePasswordService);
private readonly toastService = inject(ToastService);
submitLoading = signal(false);
submit(payload: IChangePasswordSubmitPayload) {
this.submitLoading.set(true);
this.service
.changePassword(payload.password)
.pipe(finalize(() => this.submitLoading.set(false)))
.subscribe(() => {
this.toastService.success({
text: 'تغییر رمز عبور با موفقیت انجام شد.',
});
this.submitLoading.set(false);
this.close();
});
}
}
@@ -25,32 +25,7 @@
@if (loading()) { @if (loading()) {
<shared-page-loading /> <shared-page-loading />
} @else { } @else if (error()) {
<!-- <div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4"> -->
<!-- <div class="w-full flex items-center gap-4 shrink-0 justify-between">
<div class="flex gap-2 items-center">
@if (posInfo()) {
<div class="w-10 h-10">
<img [src]="placeholderLogo" alt="Logo" class="w-full h-full object-cover rounded-md bg-surface-card" />
</div>
<span class="text-lg font-bold">
{{ posInfo()?.name }} ({{ posInfo()?.complex!.name }} - {{ posInfo()?.businessActivity!.name }})
</span>
}
</div>
<div class="flex gap-2 items-center">
<div class="bg-surface-card px-3 rounded-md shadow-sm h-9 flex items-center">
<span [jalaliDate]="now" jalaliFormat="dddd YYYY/MM/DD" class="text-base"></span>
</div>
<div class="">
<p-menu #menu [model]="profileMenuItems" [popup]="true" />
<button pButton (click)="menu.toggle($event)" icon="pi pi-user" severity="contrast">
{{ posProfile()?.account?.username }}
</button>
</div>
</div>
</div> -->
@if (error()) {
@switch (error()?.status) { @switch (error()?.status) {
@case (412) { @case (412) {
<pos-choose-cards class="h-full" (onChoose)="onChoosePos()" /> <pos-choose-cards class="h-full" (onChoose)="onChoosePos()" />
@@ -68,9 +43,9 @@
</div> </div>
} }
} }
} @else { } @else {
<pos-main-menu-sidebar [(visible)]="mainMenuVisible" /> <pos-main-menu-sidebar [(visible)]="mainMenuVisible" />
<pos-change-password [(visible)]="changePasswordVisible" />
<router-outlet></router-outlet> <router-outlet></router-outlet>
}
<!-- </div> -->
} }
<!-- </div> -->
@@ -11,6 +11,7 @@ import { Menu } from 'primeng/menu';
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
import images from 'src/assets/images'; import images from 'src/assets/images';
import config from 'src/config'; import config from 'src/config';
import { PosChangePasswordComponent } from '../../components/change-password.component';
import { PosInfoStore, PosProfileStore } from '../../store'; import { PosInfoStore, PosProfileStore } from '../../store';
import { DeviceInfoStore } from '../../store/device.store'; import { DeviceInfoStore } from '../../store/device.store';
import { PosMainMenuSidebarComponent } from '../mainMenuSidebar/main-menu-sidebar.component'; import { PosMainMenuSidebarComponent } from '../mainMenuSidebar/main-menu-sidebar.component';
@@ -30,6 +31,7 @@ import { PosChooseCardsComponent } from './choose-pos.component';
CommonModule, CommonModule,
RouterLinkWithHref, RouterLinkWithHref,
RouterOutlet, RouterOutlet,
PosChangePasswordComponent,
], ],
}) })
export class PosPagesLayoutComponent { export class PosPagesLayoutComponent {
@@ -49,6 +51,7 @@ export class PosPagesLayoutComponent {
private readonly layoutService = inject(LayoutService); private readonly layoutService = inject(LayoutService);
mainMenuVisible = signal(false); mainMenuVisible = signal(false);
changePasswordVisible = signal(false);
readonly posProfileLoading = computed(() => this.posProfileStore.loading()); readonly posProfileLoading = computed(() => this.posProfileStore.loading());
readonly posProfile = computed(() => this.posProfileStore.entity()); readonly posProfile = computed(() => this.posProfileStore.entity());
@@ -67,12 +70,21 @@ export class PosPagesLayoutComponent {
this.authService.logout(); this.authService.logout();
}; };
showChangePasswordDialog = () => {
this.changePasswordVisible.set(true);
};
profileMenuItems: MenuItem[] = [ profileMenuItems: MenuItem[] = [
{ {
label: 'راهنمای سامانه', label: 'راهنمای سامانه',
icon: 'pi pi-question-circle', icon: 'pi pi-question-circle',
disabled: true, disabled: true,
}, },
{
label: 'تغییر رمز عبور',
icon: 'pi pi-key',
command: this.showChangePasswordDialog,
},
{ {
label: 'خروج', label: 'خروج',
icon: 'pi pi-sign-out', icon: 'pi pi-sign-out',
@@ -97,7 +109,7 @@ export class PosPagesLayoutComponent {
.pipe( .pipe(
finalize(() => { finalize(() => {
this.layoutService.changeFullPageLoading(false); this.layoutService.changeFullPageLoading(false);
}), })
) )
.subscribe(); .subscribe();
}, },
@@ -1,11 +0,0 @@
<app-card-data cardTitle="تغییر گذرواژه" class="w-full">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<shared-password-input
[passwordControl]="form.controls.password"
[confirmPasswordControl]="form.controls.confirmPassword" />
<div class="flex w-full justify-end">
<button pButton label="ذخیره گذرواژه" [loading]="submitLoading()" class="w-full max-w-40"></button>
</div>
</form>
</app-card-data>
@@ -1,51 +0,0 @@
import { MustMatch } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses';
import { AppCardComponent } from '@/shared/components';
import { SharedPasswordInputComponent } from '@/shared/components/passwordInput/password-input.component';
import { fieldControl } from '@/shared/constants';
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { tap } from 'rxjs';
import { IChangePasswordSubmitPayload } from './model';
import { PosChangePasswordService } from './services/main.service';
@Component({
selector: 'pos-change-password-form',
templateUrl: './form.component.html',
imports: [
ReactiveFormsModule,
SharedPasswordInputComponent,
CommonModule,
ButtonDirective,
AppCardComponent,
],
})
export class PosChangePasswordFormDialogComponent extends AbstractForm<
IChangePasswordSubmitPayload,
any
> {
private readonly changePasswordService = inject(PosChangePasswordService);
form = this.fb.group(
{
password: fieldControl.password(),
confirmPassword: fieldControl.confirmPassword(),
},
{
validators: [MustMatch('password', 'confirmPassword')],
}
);
override submitForm() {
const payload = this.form.getRawValue().password!;
return this.changePasswordService.changePassword({ password: payload }).pipe(
tap({
next: (res) => {
this.form.reset();
},
})
);
}
}
@@ -1,3 +0,0 @@
export interface IChangePasswordSubmitPayload {
password: string;
}
@@ -1,5 +1,11 @@
<div class="flex h-full w-full flex-col items-center justify-center gap-6 p-4"> <div class="bg-surface-card flex h-full w-full flex-col gap-4 rounded-b-2xl p-4">
<app-card-data cardTitle="تنظیمات صدور صورت‌حساب" class="w-full"> <p-tabs value="0">
<p-tablist>
<p-tab value="0">تنظیمات صدور صورت‌حساب</p-tab>
<p-tab value="1">تنظیمات پرینت</p-tab>
</p-tablist>
<p-tabpanels class="mt-4">
<p-tabpanel value="0">
<div class="flex w-full flex-col gap-4"> <div class="flex w-full flex-col gap-4">
@if (info()?.guild!.code.toLocaleLowerCase() === 'gold') { @if (info()?.guild!.code.toLocaleLowerCase() === 'gold') {
<pos-config-gold-price-form class="w-full" /> <pos-config-gold-price-form class="w-full" />
@@ -9,12 +15,14 @@
<hr /> <hr />
<pos-config-send-to-fiscal-activation-form class="w-full" /> <pos-config-send-to-fiscal-activation-form class="w-full" />
</div> </div>
</app-card-data> </p-tabpanel>
<pos-change-password-form class="w-full" />
<app-card-data cardTitle="تنظیمات پرینت صورت‌حساب" class="w-full"> <p-tabpanel value="1">
<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> </p-tabpanel>
</p-tabpanels>
</p-tabs>
</div> </div>
@@ -1,9 +1,8 @@
import { PosInfoStore } from '@/domains/pos/store'; import { PosInfoStore } from '@/domains/pos/store';
import { AppCardComponent } from '@/shared/components';
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Message } from 'primeng/message'; import { Message } from 'primeng/message';
import { PosChangePasswordFormDialogComponent } from '../components/changePassword/form.component'; import { TabsModule } from 'primeng/tabs';
import { PosConfigGoldPriceFormComponent } from '../components/goldPrice/form.component'; import { PosConfigGoldPriceFormComponent } from '../components/goldPrice/form.component';
import { PosConfigPrintFormComponent } from '../components/print/form.component'; import { PosConfigPrintFormComponent } from '../components/print/form.component';
import { PosConfigRapidInvoiceFormComponent } from '../components/rapidInvoice/form.component'; import { PosConfigRapidInvoiceFormComponent } from '../components/rapidInvoice/form.component';
@@ -14,12 +13,11 @@ import { PosConfigSendToFiscalActivationFormComponent } from '../components/send
templateUrl: './root.component.html', templateUrl: './root.component.html',
imports: [ imports: [
PosConfigPrintFormComponent, PosConfigPrintFormComponent,
AppCardComponent,
Message, Message,
TabsModule,
PosConfigGoldPriceFormComponent, PosConfigGoldPriceFormComponent,
PosConfigRapidInvoiceFormComponent, PosConfigRapidInvoiceFormComponent,
PosConfigSendToFiscalActivationFormComponent, PosConfigSendToFiscalActivationFormComponent,
PosChangePasswordFormDialogComponent,
], ],
}) })
export class PosConfigPageComponent { export class PosConfigPageComponent {
@@ -27,6 +25,7 @@ export class PosConfigPageComponent {
private readonly posInfoStore = inject(PosInfoStore); private readonly posInfoStore = inject(PosInfoStore);
readonly info = computed(() => this.posInfoStore.entity()); readonly info = computed(() => this.posInfoStore.entity());
activeTabIndex = 0;
returnToMainPage() { returnToMainPage() {
this.router.navigateByUrl('/'); this.router.navigateByUrl('/');
@@ -16,7 +16,7 @@
} }
} @else { } @else {
@for (good of items(); track good.id) { @for (good of items(); track good.id) {
<div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1 shadow-xs"> <div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1">
<div class="flex grow items-center gap-2"> <div class="flex grow items-center gap-2">
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
@@ -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
@@ -16,11 +16,9 @@
<div class="flex flex-wrap gap-2 px-4 pb-2"> <div class="flex flex-wrap gap-2 px-4 pb-2">
@for (filter of activeFilters(); track filter.key) { @for (filter of activeFilters(); track filter.key) {
@if (filter.key !== 'page') { @if (filter.key !== 'page') {
<p-chip <p-chip removable class="border-surface-border border" (onRemove)="removeFilter(filter.key)">
removable <span class="text-sm"> {{ filter.label }}: {{ filter.value }} </span>
[label]="filter.label + ': ' + filter.value" </p-chip>
class="border-surface-border border"
(onRemove)="removeFilter(filter.key)" />
} }
} }
</div> </div>
@@ -1,18 +1,18 @@
<p-card class="border-surface-border relative overflow-visible border p-0!"> <p-card class="border-surface-border relative overflow-visible border p-0!">
<div class="flex w-full items-center justify-between gap-4"> <div class="flex w-full items-center justify-between gap-4">
<span> {{ saleInvoice.invoice_number }} - {{ saleInvoice.code }} </span> <span class="text-lg font-bold"> {{ saleInvoice.invoice_number }}</span>
<catalog-tax-provider-status-tag [status]="saleInvoice.status.value" [translate]="saleInvoice.status.translate" /> <catalog-tax-provider-status-tag [status]="saleInvoice.status.value" [translate]="saleInvoice.status.translate" />
</div> </div>
<hr /> <hr />
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-3">
<app-key-value alignment="end" label="نوع صورت‌حساب"> <app-key-value alignment="end" label="تاریخ" [value]="saleInvoice.created_at" type="date" />
{{ preparedInvoiceType() }}
</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="date" /> <app-key-value alignment="end" label="مشتری">
{{ preparedInvoiceType() }}
</app-key-value>
</div> </div>
<hr /> <hr />
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
@@ -50,20 +50,20 @@ export class SaleInvoiceCardComponent {
); );
preparedInvoiceType = computed(() => { preparedInvoiceType = computed(() => {
if (!this.saleInvoice) return ''; if (!this.saleInvoice) return '';
const { customer } = this.saleInvoice; const { customer, unknown_customer } = this.saleInvoice;
if (customer) { if (unknown_customer?.name || unknown_customer?.national_code) {
const { legal, individual } = customer; return `${unknown_customer.name || unknown_customer?.national_code || 'رهگذر'} (نوع دوم)`;
let text = 'نوع اول - '; } else if (customer) {
if (legal) { const { legal, individual, type } = customer;
text += `حقوقی (${legal.company_name})`; switch (type) {
case 'INDIVIDUAL':
return `${individual ? individual.first_name + ' ' + individual.last_name : '-----'} (نوع اول)`;
case 'LEGAL':
return `${legal?.name || '-----'} (نوع اول)`;
} }
if (individual) {
text += `حقیقی (${individual.first_name} ${individual.last_name})`;
} }
return text; return 'رهگذر (نوع دوم)';
}
return 'نوع دوم (بدون اطلاعات خریدار)';
}); });
sendInvoice() { sendInvoice() {
@@ -103,7 +103,7 @@ export class SaleInvoiceCardComponent {
.retrySendToTsp(this.saleInvoice.id) .retrySendToTsp(this.saleInvoice.id)
.pipe(finalize(() => this.resendingLoading.set(false))) .pipe(finalize(() => this.resendingLoading.set(false)))
.subscribe((res) => { .subscribe((res) => {
if (res.status === 'FAILURE') { if (res.status === 'SEND_FAILURE') {
this.toastService.error({ this.toastService.error({
text: res.message || 'خطا در ارسال مجدد صورت‌حساب‌ رخ داده است.', text: res.message || 'خطا در ارسال مجدد صورت‌حساب‌ رخ داده است.',
}); });
+8 -15
View File
@@ -2,6 +2,7 @@ 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 { IEnumTranslate } from '@/shared/models/enum_translate.type'; import { IEnumTranslate } from '@/shared/models/enum_translate.type';
import { IIndividualCustomer, ILegalCustomer } from '../../shop/models';
export interface IPosSaleInvoicesSummaryRawResponse { export interface IPosSaleInvoicesSummaryRawResponse {
id: string; id: string;
@@ -10,6 +11,7 @@ export interface IPosSaleInvoicesSummaryRawResponse {
invoice_date: string; invoice_date: string;
created_at: string; created_at: string;
total_amount: string; total_amount: string;
unknown_customer: Partial<UnknownCoustomer>;
customer: Maybe<Customer>; customer: Maybe<Customer>;
status: IEnumTranslate<TspProviderResponseStatus>; status: IEnumTranslate<TspProviderResponseStatus>;
type: IEnumTranslate<InvoiceTypes>; type: IEnumTranslate<InvoiceTypes>;
@@ -30,6 +32,10 @@ export interface IPosSaleInvoicesRawResponse extends IPosSaleInvoicesSummaryRawR
export interface IPosSaleInvoicesResponse extends IPosSaleInvoicesRawResponse {} export interface IPosSaleInvoicesResponse extends IPosSaleInvoicesRawResponse {}
interface UnknownCoustomer {
name?: string;
national_code?: string;
}
interface ConsumerAccount { interface ConsumerAccount {
role: string; role: string;
account: Account; account: Account;
@@ -55,19 +61,6 @@ interface Payments {
interface Customer { interface Customer {
type: string; type: string;
individual: Individual; individual: IIndividualCustomer;
legal: Legal; legal: ILegalCustomer;
}
interface Individual {
first_name: string;
last_name: string;
national_code: string;
mobile_number: string;
}
interface Legal {
company_name: string;
economic_code: string;
registration_number: string;
} }
@@ -1,13 +1,13 @@
<div class="p-4"> <shared-sale-invoice-single-view
<shared-sale-invoice-single-view
[loading]="loading()" [loading]="loading()"
[invoice]="invoice()" [invoice]="invoice()"
[backRoute]="backRoute()" [backRoute]="backRoute()"
[sendToTspLoading]="sendToTspLoading()" [sendToTspLoading]="sendToTspLoading()"
[inquiryLoading]="inquiryLoading()" [inquiryLoading]="inquiryLoading()"
[resendToTspLoading]="resendToTspLoading()"
(onSendToTsp)="sendToTsp()" (onSendToTsp)="sendToTsp()"
(onResendToTsp)="resendToTsp()"
(onInquiry)="inquiry()" (onInquiry)="inquiry()"
(onCorrection)="correction($event)" (onCorrection)="correction($event)"
(onReturnFromSale)="returnFromSale($event)" (onReturnFromSale)="returnFromSale($event)"
variant="pos" /> variant="pos" />
</div>
@@ -78,7 +78,12 @@ export class PosSaleInvoiceComponent {
this.resendToTspLoading.set(false); this.resendToTspLoading.set(false);
}) })
) )
.subscribe(); .subscribe((res) => {
this.toastService.success({
text: res.message || 'ارسال مجدد صورت‌حساب با موفقیت انجام شد.',
});
this.store.updateStatus(res.status);
});
} }
correction(data: IPosCorrectionRequest) { correction(data: IPosCorrectionRequest) {
this.correctionLoading.set(true); this.correctionLoading.set(true);
@@ -1,21 +1,20 @@
<div class="border border-surface-border rounded-xl p-2 shadow-sm"> <div class="border-surface-border rounded-xl border p-2">
<div class="flex gap-3 items-center"> <div class="flex items-center gap-3">
<img <img
[src]="inOrderGood.good.image_url || placeholderImage" [src]="inOrderGood.good.image_url || placeholderImage"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
class="w-24 h-24 rounded-md bg-surface-100 object-cover shrink-0 border border-surface-border" class="bg-surface-100 border-surface-border h-24 w-24 shrink-0 rounded-md border object-cover" />
/> <div class="flex grow flex-col justify-between overflow-hidden">
<div class="flex flex-col grow justify-between overflow-hidden"> <div class="flex grow items-stretch gap-4">
<div class="flex items-stretch grow gap-4"> <div class="flex grow flex-col justify-between gap-2 overflow-hidden py-1">
<div class="flex flex-col justify-between gap-2 grow overflow-hidden py-1"> <div class="flex w-full shrink-0 flex-col overflow-hidden">
<div class="shrink-0 flex flex-col w-full overflow-hidden"> <div class="flex w-full items-center gap-1 overflow-hidden">
<div class="flex w-full overflow-hidden gap-1 items-center"> <span class="overflow-hidden text-lg font-bold text-nowrap text-ellipsis">
<span class="font-bold text-lg text-ellipsis text-nowrap overflow-hidden">
{{ inOrderGood.good.name }} {{ inOrderGood.good.name }}
</span> </span>
</div> </div>
<span class="text-sm text-ellipsis text-nowrap overflow-hidden"> <span class="overflow-hidden text-sm text-nowrap text-ellipsis">
{{ inOrderGood.quantity }} {{ inOrderGood.good.measure_unit.name }} {{ inOrderGood.quantity }} {{ inOrderGood.good.measure_unit.name }}
</span> </span>
</div> </div>
@@ -23,7 +22,7 @@
<span [appPriceMask]="inOrderGood.total_amount"></span> <span [appPriceMask]="inOrderGood.total_amount"></span>
} @else { } @else {
<div class="flex flex-col"> <div class="flex flex-col">
<span class="line-through text-muted-color text-xs" [appPriceMask]="inOrderGood.base_total_amount"></span> <span class="text-muted-color text-xs line-through" [appPriceMask]="inOrderGood.base_total_amount"></span>
<span [appPriceMask]="inOrderGood.base_total_amount - inOrderGood.discount_amount"></span> <span [appPriceMask]="inOrderGood.base_total_amount - inOrderGood.discount_amount"></span>
</div> </div>
} }
@@ -36,8 +35,7 @@
size="small" size="small"
severity="secondary" severity="secondary"
class="ms-auto" class="ms-auto"
(click)="editOrderItem()" (click)="editOrderItem()"></button>
></button>
<button <button
pButton pButton
type="button" type="button"
@@ -45,8 +43,7 @@
size="small" size="small"
severity="danger" severity="danger"
class="ms-auto" class="ms-auto"
(click)="removeGoodFromOrder()" (click)="removeGoodFromOrder()"></button>
></button>
</div> </div>
</div> </div>
</div> </div>
@@ -58,6 +55,5 @@
[(visible)]="visible" [(visible)]="visible"
[good]="inOrderGood.good" [good]="inOrderGood.good"
[initialValues]="inOrderGood" [initialValues]="inOrderGood"
[orderItemId]="inOrderGood.id" [orderItemId]="inOrderGood.id" />
/>
} }
@@ -1,8 +1,15 @@
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject } from '@angular/core'; import { Component, Input, inject } from '@angular/core';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { PosLandingStore } from '../../store/main.store'; import { PosLandingStore } from '../../store/main.store';
type TOrderPriceInfo = {
totalBaseAmount: number;
discountAmount: number;
taxAmount: number;
totalAmount: number;
};
@Component({ @Component({
selector: 'pos-order-price-info-card', selector: 'pos-order-price-info-card',
templateUrl: './price-info-card.component.html', templateUrl: './price-info-card.component.html',
@@ -10,6 +17,12 @@ import { PosLandingStore } from '../../store/main.store';
}) })
export class POSOrderPriceInfoCardComponent { export class POSOrderPriceInfoCardComponent {
private readonly store = inject(PosLandingStore); private readonly store = inject(PosLandingStore);
@Input() info?: TOrderPriceInfo;
priceInfo = computed(() => this.store.orderPricingInfo()); priceInfo(): TOrderPriceInfo {
if (this.info) {
return this.info;
}
return this.store.orderPricingInfo();
}
} }
@@ -1,31 +1,32 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="w-full overflow-visible flex gap-3 px-4"> <div class="flex w-full gap-3 overflow-visible px-4">
@if (loading()) { @if (loading()) {
@for (i of [1, 2, 3, 4, 5]; track i) { @for (i of [1, 2, 3, 4, 5]; track i) {
<p-skeleton width="6rem" height="2.5rem" /> <div class="h-10 w-24 shrink-0">
<p-skeleton width="100%" height="100%" />
</div>
} }
} @else { } @else {
@for (category of categories(); track category.id) { @for (category of categories(); track category.id) {
<div <div
[class]=" [class]="
'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all shrink-0' + 'border-surface-border bg-surface-100 hover:bg-surface-200 flex h-10 shrink-0 cursor-pointer items-center gap-3 rounded-lg border px-3 transition-all' +
(selectedCategory() === category.id ? ' bg-surface-card' : '') (selectedCategory() === category.id ? ' bg-surface-card' : '')
" "
(click)="changeSelectedCategory(category)" (click)="changeSelectedCategory(category)">
> <span class="text-muted-color text-sm font-bold"> {{ category.name }}</span>
<span class="text-sm text-muted-color font-bold"> {{ category.name }}</span>
<div <div
[class]=" [class]="
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' + 'bg-surface-300 text-muted-color flex items-center justify-center rounded-sm px-2 py-0.5 text-xs' +
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '') (selectedCategory() === category.id ? ' bg-amber-400! text-white' : '')
" ">
>
<span> <span>
{{ category.goods_count }} {{ category.goods_count }}
</span> </span>
</div> </div>
</div> </div>
} }
<div class="invisible w-1 shrink-0 opacity-0"></div>
} }
</div> </div>
</div> </div>
@@ -7,7 +7,7 @@
} }
} @else { } @else {
@for (good of visibleGoods(); track good.id) { @for (good of visibleGoods(); track good.id) {
<div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1 shadow-xs"> <div class="bg-surface-card flex items-center gap-2 rounded-lg px-3 py-1">
<div class="flex grow items-center gap-2"> <div class="flex grow items-center gap-2">
<img <img
[src]="good.image_url || goodPlaceholder" [src]="good.image_url || goodPlaceholder"
@@ -18,7 +18,7 @@
<button <button
type="button" type="button"
pButton pButton
class="border-surface-border bg-surface-0 sticky! inset-x-0 right-0 bottom-0 z-1200 w-full rounded-b-none! border-t px-4 py-3 shadow-[0_-4px_16px_rgba(0,0,0,0.08)] md:hidden!" class="border-surface-border bg-surface-0 sticky! inset-x-0 right-0 bottom-0 z-120 w-full rounded-b-none! border-t px-4 py-3 md:hidden!"
(click)="openInvoiceBottomSheet()"> (click)="openInvoiceBottomSheet()">
<!-- [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">
@@ -17,7 +17,7 @@ import { PosLandingStore } from '../store/main.store';
@Component({ @Component({
selector: 'pos-shop', selector: 'pos-shop',
templateUrl: './root.component.html', templateUrl: './root.component.html',
host: { class: 'grow overflow-hidden' }, host: { class: 'grow' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
@@ -1,5 +1,5 @@
<div <div
class="text-surface-700 relative flex flex-col gap-4 overflow-hidden rounded-lg border px-4 py-4 pe-2" class="text-surface-700 relative flex h-full flex-col gap-4 overflow-hidden rounded-lg border py-4 ps-3 pe-2"
[style.backgroundColor]="preparedInfo().bgColor" [style.backgroundColor]="preparedInfo().bgColor"
[style.borderColor]="preparedInfo().borderColor" [style.borderColor]="preparedInfo().borderColor"
(click)="showDetails()"> (click)="showDetails()">
@@ -1,6 +1,10 @@
<div class="flex flex-col gap-6 p-4"> <div class="flex min-h-[calc(100svh-5.5rem)] flex-col p-4 pt-0!">
<season-picker (onChange)="onDateChanged($event)" /> <div class="bg-surface-ground sticky top-[5.5rem] z-10 w-full pt-4!">
<div class="grid grid-cols-2 gap-4"> <button pButton [routerLink]="shopRoute" size="large" class="w-full">ایجاد صورت‌حساب جدید</button>
</div>
<season-picker class="mt-6 mb-4 block w-full" (onChange)="onDateChanged($event)" />
<div class="flex-1">
<div class="grid grid-cols-2 items-stretch gap-2">
@if (loading()) { @if (loading()) {
@for (_ of [0, 1, 2, 3, 4, 5]; track $index) { @for (_ of [0, 1, 2, 3, 4, 5]; track $index) {
<div class="h-32 w-full"> <div class="h-32 w-full">
@@ -51,7 +55,5 @@
[count]="item()?.not_original?.count" /> [count]="item()?.not_original?.count" />
} }
</div> </div>
<div class="fixed inset-x-0 bottom-0 p-4">
<button pButton [routerLink]="shopRoute" size="large" class="w-full">ایجاد صورت‌حساب جدید</button>
</div> </div>
</div> </div>
@@ -1,7 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { IChangePasswordSubmitPayload } from '../model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -9,7 +8,7 @@ import { IChangePasswordSubmitPayload } from '../model';
export class PosChangePasswordService { export class PosChangePasswordService {
constructor(private readonly http: HttpClient) {} constructor(private readonly http: HttpClient) {}
changePassword(payload: IChangePasswordSubmitPayload): Observable<unknown> { changePassword(password: string): Observable<unknown> {
return this.http.put('/api/v1/pos/update-password', payload); return this.http.put('/api/v1/pos/update-password', { password });
} }
} }
@@ -1,23 +1,22 @@
<div class="layout-wrapper" [ngClass]="containerClass"> <div class="layout-wrapper flex min-h-svh flex-col" [ngClass]="containerClass">
@if (!fullPageLoading()) { @if (!fullPageLoading()) {
<app-topbar <app-topbar
[showMenu]="showMenu()" [showMenu]="showMenu()"
[startTemplate]="topBarStartAction" [startTemplate]="topBarStartAction"
[centerTemplate]="topBarCenterAction" [centerTemplate]="topBarCenterAction"
[endTemplate]="topBarEndAction || topBarMoreAction" [endTemplate]="topBarEndAction || topBarMoreAction" />
/>
} }
@if (showMenu()) { @if (showMenu()) {
<app-sidebar></app-sidebar> <app-sidebar></app-sidebar>
} }
<div [class]="`layout-main-container ${!showMenu() ? 'hideMenu' : ''} grow ${isFullPage ? 'isFullPage' : ''}`"> <div
<div class="layout-main flex flex-col gap-4"> [class]="`flex flex-col justify-between ${!showMenu() ? 'hideMenu m0 ps-0' : ''} grow ${isFullPage ? 'isFullPage p-0 ps-0' : ''}`">
<div class="flex flex-1 flex-col gap-4">
@if (showBreadcrumb) { @if (showBreadcrumb) {
<app-breadcrumb class="rounded-md overflow-hidden shrink-0" /> <app-breadcrumb class="shrink-0 overflow-hidden rounded-md" />
} }
<div [class]="`rounded-md flex flex-col grow`"> <div [class]="`flex min-h-0 flex-1 flex-col rounded-md`">
<!-- style="container-type: size" --> <!-- style="container-type: size" -->
<div class="h-full">
@if (content) { @if (content) {
<ng-container [ngTemplateOutlet]="content"></ng-container> <ng-container [ngTemplateOutlet]="content"></ng-container>
} @else { } @else {
@@ -25,7 +24,6 @@
} }
</div> </div>
</div> </div>
</div>
<app-footer></app-footer> <app-footer></app-footer>
</div> </div>
<div class="layout-mask animate-fadein"></div> <div class="layout-mask animate-fadein"></div>
@@ -1,3 +1,3 @@
<div class="p-4"> <div class="min-h-dvh p-4">
<shared-sale-invoice-single-view [loading]="loading()" [invoice]="invoice()" /> <shared-sale-invoice-single-view [loading]="loading()" [invoice]="invoice()" />
</div> </div>
@@ -1,10 +1,10 @@
import { MustMatch, password } from '@/core/validators'; import { MustMatch, password } from '@/core/validators';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog'; import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
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 { Component, EventEmitter, Output, input } from '@angular/core'; import { Component, EventEmitter, Output, input } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { SharedPasswordInputComponent } from '../passwordInput/password-input.component'; import { SharedPasswordInputComponent } from '../passwordInput/password-input.component';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component';
export interface IChangePasswordSubmitPayload { export interface IChangePasswordSubmitPayload {
password: string; password: string;
@@ -14,7 +14,12 @@ export interface IChangePasswordSubmitPayload {
@Component({ @Component({
selector: 'shared-change-password-form-dialog', selector: 'shared-change-password-form-dialog',
templateUrl: './change-password-form-dialog.component.html', templateUrl: './change-password-form-dialog.component.html',
imports: [ReactiveFormsModule, SharedDialogComponent, FormFooterActionsComponent, SharedPasswordInputComponent], imports: [
ReactiveFormsModule,
SharedDialogComponent,
FormFooterActionsComponent,
SharedPasswordInputComponent,
],
}) })
export class ChangePasswordFormDialogComponent extends AbstractDialog { export class ChangePasswordFormDialogComponent extends AbstractDialog {
title = input<string>(''); title = input<string>('');
@@ -31,11 +36,11 @@ export class ChangePasswordFormDialogComponent extends AbstractDialog {
}, },
{ {
validators: [MustMatch('password', 'confirmPassword')], validators: [MustMatch('password', 'confirmPassword')],
}, }
); );
get dialogHeader() { get dialogHeader() {
return this.title() ? `تغییر گذرواژه ${this.title()}` : 'تغییر گذرواژه'; return this.title() ? `تغییر رمز عبور ${this.title()}` : 'تغییر رمز عبور';
} }
submit() { submit() {
@@ -4,7 +4,7 @@
<div class="bg-surface-card 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 rotate-y-180 text-5xl!"></i>
</div> </div>
<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>
@@ -1,6 +1,7 @@
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div> @if (rendered) {
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div>
<section class="light-bottomsheet-panel" [ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }"> <section class="light-bottomsheet-panel" [ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }">
<div class="light-bottomsheet-content"> <div class="light-bottomsheet-content">
@if (header || closable) { @if (header || closable) {
<header class="light-bottomsheet-header border-surface-border border-b"> <header class="light-bottomsheet-header border-surface-border border-b">
@@ -22,4 +23,5 @@
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div> </div>
</section> </section>
}
@@ -90,8 +90,8 @@ import { Button } from 'primeng/button';
'[attr.pfocustrap]': 'true', '[attr.pfocustrap]': 'true',
'[attr.data-pc-name]': "'drawer'", '[attr.data-pc-name]': "'drawer'",
'[attr.data-pc-section]': "'root'", '[attr.data-pc-section]': "'root'",
'[attr.aria-hidden]': '!visible', '[attr.aria-hidden]': '!rendered',
'[style.display]': 'visible ? "block" : "none"', '[style.display]': 'rendered ? "block" : "none"',
'[style.--sheet-transition]': 'transitionOptions', '[style.--sheet-transition]': 'transitionOptions',
}, },
}) })
@@ -105,8 +105,11 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
@Input() style: Record<string, string> | undefined; @Input() style: Record<string, string> | undefined;
@Input() mobileDrawerHeight = '90vh'; @Input() mobileDrawerHeight = '90vh';
@Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)'; @Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)';
@Input() closeUnmountDelay = 150;
@Output() onHide = new EventEmitter<any>(); @Output() onHide = new EventEmitter<any>();
rendered = false;
private closeTimer: ReturnType<typeof setTimeout> | null = null;
private originalParent: Node | null = null; private originalParent: Node | null = null;
private readonly defaultDrawerZIndex = 1102; private readonly defaultDrawerZIndex = 1102;
@@ -123,21 +126,27 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
const host = this.elementRef.nativeElement; const host = this.elementRef.nativeElement;
this.originalParent = host.parentNode; this.originalParent = host.parentNode;
this.renderer.appendChild(this.document.body, host); this.renderer.appendChild(this.document.body, host);
this.rendered = this.visible;
this.syncZIndex(); this.syncZIndex();
this.toggleBodyScrollLock(this.visible); this.toggleBodyScrollLock(this.visible);
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['visible'] && this.visible) { if (changes['visible'] && this.visible) {
this.clearCloseTimer();
this.rendered = true;
this.syncZIndex(); this.syncZIndex();
} }
if (changes['visible']) { if (changes['visible']) {
this.toggleBodyScrollLock(this.visible); this.toggleBodyScrollLock(this.visible);
if (!this.visible) {
this.scheduleUnmount();
}
} }
} }
ngOnDestroy() { ngOnDestroy() {
this.clearCloseTimer();
this.toggleBodyScrollLock(false); this.toggleBodyScrollLock(false);
this.removeDrawer(); this.removeDrawer();
} }
@@ -154,9 +163,12 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
this.visibleChange.emit(nextValue); this.visibleChange.emit(nextValue);
this.toggleBodyScrollLock(nextValue); this.toggleBodyScrollLock(nextValue);
if (nextValue) { if (nextValue) {
this.clearCloseTimer();
this.rendered = true;
this.syncZIndex(); this.syncZIndex();
} }
if (!nextValue) { if (!nextValue) {
this.scheduleUnmount();
this.onHide.emit(); this.onHide.emit();
} }
} }
@@ -203,4 +215,19 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
: this.defaultDrawerZIndex; : this.defaultDrawerZIndex;
this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`); this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`);
} }
private scheduleUnmount() {
this.clearCloseTimer();
this.closeTimer = setTimeout(() => {
if (!this.visible) {
this.rendered = false;
}
}, this.closeUnmountDelay);
}
private clearCloseTimer() {
if (!this.closeTimer) return;
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
} }
@@ -3,7 +3,7 @@
<button <button
pButton pButton
[label]="submitLabel" [label]="submitLabel"
type="submit" [type]="submitBtnType"
class="min-w-28" class="min-w-28"
[disabled]="disabled" [disabled]="disabled"
[loading]="loading" [loading]="loading"
@@ -11,6 +11,7 @@ export class FormFooterActionsComponent {
@Input() cancelLabel = 'انصراف'; @Input() cancelLabel = 'انصراف';
@Input() loading = false; @Input() loading = false;
@Input() disabled = false; @Input() disabled = false;
@Input() submitBtnType: 'button' | 'submit' = 'submit';
@Output() onSubmit = new EventEmitter<void>(); @Output() onSubmit = new EventEmitter<void>();
@Output() onCancel = new EventEmitter<void>(); @Output() onCancel = new EventEmitter<void>();
@@ -1,4 +1,4 @@
<div class="flex items-center p-4"> <div class="bg-surface-card flex items-center p-4">
<div class="grow"> <div class="grow">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if (backRoute) { @if (backRoute) {
@@ -1,28 +1,78 @@
<form [formGroup]="form" (ngSubmit)="submit()"> <form [formGroup]="form" (ngSubmit)="submit()">
<field-invoice-date [control]="form.controls.invoice_date" /> <field-invoice-date [control]="form.controls.invoice_date" />
<div class="mt-4 flex flex-col gap-3">
@for (item of initialValues || []; track item.id) { @for (item of initialValues || []; track item.id) {
<div class="py-3"> <div class="border-surface-border rounded-xl border p-2">
@if ($index) { <div class="flex items-center gap-3">
<hr class="h-1 w-full" /> <img
} [src]="item.good_snapshot.image_url || ''"
<p-divider align="right"> loading="lazy"
{{ $index + 1 }}- <b>{{ item.good_snapshot.name }}</b> decoding="async"
</p-divider> class="bg-surface-100 border-surface-border h-20 w-20 shrink-0 rounded-md border object-cover" />
@if (isGold(item)) { <div class="flex grow flex-col gap-2 overflow-hidden py-1">
<shared-gold-payload-form <div class="flex items-center justify-between gap-2">
[initialValues]="$any(mappedInitialItems[$index])" <span class="overflow-hidden text-base font-bold text-nowrap text-ellipsis">
[vatPercentage]="vatFromItem(item)" {{ item.good_snapshot.name }}
[isCorrection]="true" /> </span>
<button
pButton
type="button"
icon="pi pi-pencil"
size="small"
outlined
(click)="openItemEditor($index)"></button>
</div>
@let preview = getItemPreview($index);
<span class="text-sm">{{ item.quantity }} {{ item.measure_unit_text }}</span>
<div class="flex flex-col">
@if (!preview.discount_amount) {
<span [appPriceMask]="preview.base_total_amount"></span>
} @else { } @else {
<shared-standard-payload-form <div class="flex flex-col items-end">
[initialValues]="$any(mappedInitialItems[$index])" <span class="text-muted-color text-xs line-through" [appPriceMask]="preview.base_total_amount"></span>
[measureUnit]="item.good_snapshot.measure_unit" <span [appPriceMask]="preview.base_total_amount - preview.discount_amount"></span>
[vatPercentage]="vatFromItem(item)"
[isCorrection]="true" />
}
</div> </div>
} }
</div>
</div>
</div>
</div>
}
</div>
<pos-order-price-info-card [info]="getTotalPriceInfo()" />
<app-form-footer-actions /> <app-form-footer-actions />
</form> </form>
<shared-light-bottomsheet
[(visible)]="showItemEditSheet"
[header]="`ویرایش ${activeItem()?.good_snapshot?.name || ''}`">
@if (activeItem()) {
<div class="pt-4">
@if (isGold(activeItem()!)) {
<shared-gold-payload-form
#goldEditor
[initialValues]="$any(activeInitialValue())"
[vatPercentage]="vatFromItem(activeItem()!)"
[isCorrection]="true" />
<div class="mt-6 flex justify-end gap-2">
<button pButton type="button" severity="secondary" (click)="backToList()">انصراف</button>
<button pButton type="button" (click)="saveActiveItemDraft(goldEditor)">ذخیره تغییرات</button>
</div>
} @else {
<shared-standard-payload-form
#standardEditor
[initialValues]="$any(activeInitialValue())"
[measureUnit]="activeItem()!.good_snapshot.measure_unit"
[vatPercentage]="vatFromItem(activeItem()!)"
[isCorrection]="true" />
<div class="mt-6 flex justify-end gap-2">
<button pButton type="button" severity="secondary" (click)="backToList()">انصراف</button>
<button pButton type="button" (click)="saveActiveItemDraft(undefined, standardEditor)">ذخیره تغییرات</button>
</div>
}
</div>
}
</shared-light-bottomsheet>
@@ -1,16 +1,19 @@
import ISummary from '@/core/models/summary'; import ISummary from '@/core/models/summary';
import { POSOrderPriceInfoCardComponent } from '@/domains/pos/modules/shop/components/order/price-info-card.component';
import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { IPosOrderItem } from '@/domains/pos/modules/shop/models';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { import {
SharedGoldPayloadFormComponent, SharedGoldPayloadFormComponent,
SharedStandardPayloadFormComponent, SharedStandardPayloadFormComponent,
} from '@/shared/components'; } from '@/shared/components';
import { PriceMaskDirective } from '@/shared/directives';
import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType'; import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType';
import { IGoldPayload, IStandardPayload } from '@/shared/models'; import { IGoldPayload, IStandardPayload } from '@/shared/models';
import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils'; import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils';
import { Component, Input, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { Component, Input, signal, SimpleChanges } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { Divider } from 'primeng/divider'; import { ButtonDirective } from 'primeng/button';
import { SharedLightBottomsheetComponent } from '../../dialog';
import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component'; import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component';
import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component';
import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models'; import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models';
@@ -25,7 +28,10 @@ import { IInvoiceItem } from '../sale-invoice-full-response.model';
FormFooterActionsComponent, FormFooterActionsComponent,
SharedGoldPayloadFormComponent, SharedGoldPayloadFormComponent,
SharedStandardPayloadFormComponent, SharedStandardPayloadFormComponent,
Divider, ButtonDirective,
SharedLightBottomsheetComponent,
POSOrderPriceInfoCardComponent,
PriceMaskDirective,
], ],
}) })
export class SharedCorrectionFormComponent extends AbstractForm< export class SharedCorrectionFormComponent extends AbstractForm<
@@ -34,11 +40,7 @@ export class SharedCorrectionFormComponent extends AbstractForm<
IInvoiceItem[] IInvoiceItem[]
> { > {
@Input({ required: true }) invoiceDate!: string; @Input({ required: true }) invoiceDate!: string;
@Input() visible = false;
@ViewChildren(SharedGoldPayloadFormComponent)
goldForms!: QueryList<SharedGoldPayloadFormComponent>;
@ViewChildren(SharedStandardPayloadFormComponent)
standardForms!: QueryList<SharedStandardPayloadFormComponent>;
form = this.fb.group({ form = this.fb.group({
invoice_date: [this.invoiceDate], invoice_date: [this.invoiceDate],
@@ -46,6 +48,8 @@ export class SharedCorrectionFormComponent extends AbstractForm<
itemDrafts: Record<string, TCorrectionItemPayload> = {}; itemDrafts: Record<string, TCorrectionItemPayload> = {};
mappedInitialItems: TCorrectionItemPayload[] = []; mappedInitialItems: TCorrectionItemPayload[] = [];
showItemEditSheet = signal(false);
activeItemIndex = signal<number | null>(null);
initForm() { initForm() {
this.form.controls.invoice_date.setValue( this.form.controls.invoice_date.setValue(
@@ -53,6 +57,8 @@ export class SharedCorrectionFormComponent extends AbstractForm<
); );
this.itemDrafts = {}; this.itemDrafts = {};
this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item)); this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item));
this.showItemEditSheet.set(false);
this.activeItemIndex.set(null);
} }
override ngOnInit(): void { override ngOnInit(): void {
@@ -60,6 +66,11 @@ export class SharedCorrectionFormComponent extends AbstractForm<
} }
override ngOnChanges(changes: SimpleChanges) { override ngOnChanges(changes: SimpleChanges) {
if (changes['visible'] && !this.visible) {
this.initForm();
return;
}
if (changes['initialValues'] || changes['invoiceDate']) { if (changes['initialValues'] || changes['invoiceDate']) {
this.initForm(); this.initForm();
} }
@@ -115,23 +126,88 @@ export class SharedCorrectionFormComponent extends AbstractForm<
return this.mapToStandardInitialItem(item) as TCorrectionItemPayload; return this.mapToStandardInitialItem(item) as TCorrectionItemPayload;
} }
override submitForm() { openItemEditor(index: number) {
const goldQueue = [...(this.goldForms?.toArray() || [])]; this.activeItemIndex.set(index);
const standardQueue = [...(this.standardForms?.toArray() || [])]; this.showItemEditSheet.set(true);
const items = }
this.initialValues?.map((item, index) => {
backToList() {
this.showItemEditSheet.set(false);
this.activeItemIndex.set(null);
}
activeItem() {
const index = this.activeItemIndex();
if (index === null) return null;
return this.initialValues?.[index] || null;
}
activeInitialValue() {
const item = this.activeItem();
const index = this.activeItemIndex();
if (!item || index === null) return null;
return this.itemDrafts[item.id] || this.mappedInitialItems[index] || null;
}
getItemPreview(index: number) {
const item = this.initialValues?.[index];
return this.itemDrafts[item!.id] || this.mappedInitialItems[index] || null;
}
getTotalPriceInfo() {
const previews = (this.initialValues || [])
.map((_, index) => this.getItemPreview(index))
.filter(Boolean);
console.log('previews', previews);
return previews.reduce(
(acc, item: any) => {
acc.totalBaseAmount += Number(item.base_total_amount || 0);
acc.discountAmount += Number(item.discount_amount || 0);
acc.taxAmount += Number(item.tax_amount || 0);
acc.totalAmount += Number(item.total_amount || 0);
return acc;
},
{
totalBaseAmount: 0,
discountAmount: 0,
taxAmount: 0,
totalAmount: 0,
}
);
}
saveActiveItemDraft(
goldForm?: SharedGoldPayloadFormComponent,
standardForm?: SharedStandardPayloadFormComponent
) {
const item = this.activeItem();
const index = this.activeItemIndex();
if (!item || index === null) return;
const current = this.isGold(item) const current = this.isGold(item)
? goldQueue.shift()?.getCorrectionValue() ? goldForm?.getCorrectionValue()
: standardQueue.shift()?.getCorrectionValue(); : standardForm?.getCorrectionValue();
const merged = (current || this.mappedInitialItems[index]) as TCorrectionItemPayload;
return { if (!current) {
...merged, this.toastService.warn({ text: 'اطلاعات آیتم برای ذخیره در دسترس نیست.' });
return;
}
this.itemDrafts[item.id] = {
...(current as TCorrectionItemPayload),
id: item.id, id: item.id,
pricing_model: item.good_snapshot?.pricing_model || '', pricing_model: item.good_snapshot?.pricing_model || '',
} as TCorrectionItemPayload; };
}) || []; this.backToList();
}
console.log('items', items); override submitForm() {
const items =
this.initialValues?.map(
(item, index) => this.itemDrafts[item.id] || this.mappedInitialItems[index]
) || [];
const hasChanges = (this.initialValues || []).some((item, index) => { const hasChanges = (this.initialValues || []).some((item, index) => {
const source = this.mappedInitialItems[index]; const source = this.mappedInitialItems[index];
@@ -3,23 +3,15 @@
} @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="لطفا صبر کنید"> <app-inner-pages-header [pageTitle]="`صورت‌حساب ${invoice.invoice_number}`" [backRoute]="backRoute">
<div class="flex h-[30svh] flex-col items-center justify-center gap-6"> <ng-template #actions>
<p-progressSpinner /> <p-button (click)="printInvoice()" icon="pi pi-print" outlined size="small" label="چاپ" />
<span class="text-lg font-bold">در حال ارسال درخواست شما...</span>
</div>
</shared-light-bottomsheet>
<div class="flex flex-col gap-6">
<app-card-data cardTitle="اطلاعات صورت‌حساب‌" [editable]="false" [backRoute]="backRoute">
<ng-template #moreActions>
<div class="">
<p-menu #menu [model]="moreActionMenuItems()" [popup]="true" appendTo="body" />
<p-button (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" outlined size="small" />
</div>
</ng-template> </ng-template>
</app-inner-pages-header>
<div class="p-4">
<p-card>
<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.tax_id" /> <app-key-value label="شماره منحصر به فرد مالیاتی" [value]="invoice.tax_id" />
<app-key-value label="تاریخ صورت‌حساب‌" [value]="invoice.invoice_date" type="date" /> <app-key-value label="تاریخ صورت‌حساب‌" [value]="invoice.invoice_date" type="date" />
<app-key-value label="تاریخ ایجاد صورت‌حساب‌" [value]="invoice.created_at" type="dateTime" /> <app-key-value label="تاریخ ایجاد صورت‌حساب‌" [value]="invoice.created_at" type="dateTime" />
@@ -93,9 +85,7 @@
<p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p> <p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p>
} }
} }
</div> <p-divider align="center"> کالاهای خریداری شده </p-divider>
</app-card-data>
<app-card-data cardTitle="کالاهای خریداری شده" [editable]="false">
<app-page-data-list <app-page-data-list
[columns]="columns" [columns]="columns"
[items]="invoice.items" [items]="invoice.items"
@@ -108,8 +98,26 @@
} }
</ng-template> </ng-template>
</app-page-data-list> </app-page-data-list>
</app-card-data>
</div> </div>
</p-card>
</div>
@if (moreActionMenuItems().length) {
<div class="border-surface-border bg-surface-card sticky bottom-0 z-10 mt-4 w-full rounded-t-2xl border-t p-3">
<div class="flex flex-wrap justify-center gap-2">
@for (action of moreActionMenuItems(); track action.label || $index) {
<button
pButton
[label]="action.label || ''"
[icon]="action.icon || ''"
[severity]="$any(action).severity || 'secondary'"
[outlined]="$any(action).severity === 'secondary'"
[loading]="actionLoading"
(click)="action.command()"></button>
}
</div>
</div>
}
<shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش"> <shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش">
<shared-return-form <shared-return-form
@@ -119,6 +127,7 @@
</shared-light-bottomsheet> </shared-light-bottomsheet>
<shared-light-bottomsheet [(visible)]="showCorrectionForm" header="اصلاحی"> <shared-light-bottomsheet [(visible)]="showCorrectionForm" header="اصلاحی">
<shared-correction-form <shared-correction-form
[visible]="showCorrectionForm()"
[initialValues]="invoice.items" [initialValues]="invoice.items"
[invoiceDate]="invoice.invoice_date" [invoiceDate]="invoice.invoice_date"
(onSubmit)="correctionSubmit($event)" /> (onSubmit)="correctionSubmit($event)" />
@@ -11,11 +11,7 @@ import {
TspProviderResponseStatus, TspProviderResponseStatus,
} from '@/shared/catalog'; } from '@/shared/catalog';
import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import { import { KeyValueComponent, SharedLightBottomsheetComponent } from '@/shared/components';
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 {
@@ -38,25 +34,32 @@ import {
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { UrlTree } from '@angular/router'; import { UrlTree } from '@angular/router';
import { MenuItem } from 'primeng/api'; import { Button, ButtonDirective } from 'primeng/button';
import { Button } from 'primeng/button'; import { Card } from 'primeng/card';
import { Dialog } from 'primeng/dialog';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { Menu } from 'primeng/menu';
import { ProgressSpinner } from 'primeng/progressspinner'; import { ProgressSpinner } from 'primeng/progressspinner';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service';
import { InnerPagesHeaderComponent } from '../innerPagesHeader/inner-pages-header.component';
import { SharedCorrectionFormComponent } from './correctionForm'; import { SharedCorrectionFormComponent } from './correctionForm';
import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models'; import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models';
import { SharedReturnFormComponent } from './returnForm/form.component'; import { SharedReturnFormComponent } from './returnForm/form.component';
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
type TActionMenuItem = {
label: string;
icon: string;
command: () => void;
neededStatus?: TspProviderResponseStatus;
severity?: 'secondary' | 'info' | 'warn' | 'danger' | 'contrast';
};
@Component({ @Component({
selector: 'shared-sale-invoice-single-view', selector: 'shared-sale-invoice-single-view',
templateUrl: './sale-invoice-single-view.component.html', templateUrl: './sale-invoice-single-view.component.html',
imports: [ imports: [
AppCardComponent,
KeyValueComponent, KeyValueComponent,
Divider, Divider,
PageDataListComponent, PageDataListComponent,
@@ -66,13 +69,16 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
PriceMaskDirective, PriceMaskDirective,
CatalogTaxProviderStatusTagComponent, CatalogTaxProviderStatusTagComponent,
CatalogInvoiceTypeTagComponent, CatalogInvoiceTypeTagComponent,
Menu,
Button, Button,
CatalogInvoiceSettlementTypeTagComponent, CatalogInvoiceSettlementTypeTagComponent,
SharedLightBottomsheetComponent, SharedLightBottomsheetComponent,
ProgressSpinner, ProgressSpinner,
SharedReturnFormComponent, SharedReturnFormComponent,
SharedCorrectionFormComponent, SharedCorrectionFormComponent,
Dialog,
ButtonDirective,
InnerPagesHeaderComponent,
Card,
], ],
}) })
export class SharedSaleInvoiceSingleViewComponent { export class SharedSaleInvoiceSingleViewComponent {
@@ -100,41 +106,89 @@ export class SharedSaleInvoiceSingleViewComponent {
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>; @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
moreActionMenuItems = signal<MenuItem[]>([]); moreActionMenuItems = signal<TActionMenuItem[]>([]);
showCorrectionForm = signal(false); showCorrectionForm = signal(false);
showReturnFromSaleForm = signal(false); showReturnFromSaleForm = signal(false);
constructor() { showErrors = () => {};
this.showErrors = this.showErrors.bind(this); inquiry = () => {
this.showRevokeConfirmation = this.showRevokeConfirmation.bind(this); this.onInquiry.emit();
this.showCorrection = this.showCorrection.bind(this); };
this.showReturnFromSale = this.showReturnFromSale.bind(this); send = () => {
this.printInvoice = this.printInvoice.bind(this); this.onSendToTsp.emit();
this.send = this.send.bind(this); };
this.resend = this.resend.bind(this); sendCorrection = (event: ICorrectionRequest) => {
this.inquiry = this.inquiry.bind(this); this.onCorrection.emit(event);
} };
resend = () => {
this.onResendToTsp.emit();
};
sendReturnFromSale = (event: IPosReturnFromSaleRequest) => {
this.onReturnFromSale.emit(event);
};
showCorrection = () => {
this.showCorrectionForm.set(true);
};
showReturnFromSale = () => {
this.showReturnFromSaleForm.set(true);
};
private readonly actions: TActionMenuItem[] = [
{
label: 'ارسال مجدد صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.SEND_FAILURE,
severity: 'info',
command: () => this.resend(),
},
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.NOT_SEND,
severity: 'info',
command: () => this.send(),
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: TspProviderResponseStatus.FISCAL_QUEUED,
severity: 'info',
command: () => this.inquiry(),
},
{
label: 'دلایل خطا',
icon: 'pi pi-question',
neededStatus: TspProviderResponseStatus.FAILURE,
severity: 'danger',
command: () => this.showErrors(),
},
{
label: 'ابطال',
icon: 'pi pi-eraser',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'danger',
command: () => this.showRevokeConfirmation(),
},
{
label: 'اصلاح',
icon: 'pi pi-file-edit',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'warn',
command: () => this.showCorrection(),
},
{
label: 'برگشت از فروش',
icon: 'pi pi-cart-minus',
neededStatus: TspProviderResponseStatus.SUCCESS,
severity: 'warn',
command: () => this.showReturnFromSale(),
},
];
get actionLoading() { get actionLoading() {
return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading; return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading;
} }
showErrors() {}
inquiry() {
this.onInquiry.emit();
}
send() {
this.onSendToTsp.emit();
}
sendCorrection(event: ICorrectionRequest) {
this.onCorrection.emit(event);
}
resend() {
this.onResendToTsp.emit();
}
sendReturnFromSale(event: IPosReturnFromSaleRequest) {
this.onReturnFromSale.emit(event);
}
async showRevokeConfirmation() { async showRevokeConfirmation() {
await this.confirmationService.ask({ await this.confirmationService.ask({
header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`,
@@ -156,72 +210,13 @@ export class SharedSaleInvoiceSingleViewComponent {
}, },
}); });
} }
showCorrection() {
this.showCorrectionForm.set(true);
}
showReturnFromSale() {
this.showReturnFromSaleForm.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[] = [
{
label: 'ارسال مجدد صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.SEND_FAILURE,
command: this.resend,
},
{
label: 'ارسال صورت‌حساب',
icon: 'pi pi-send',
neededStatus: TspProviderResponseStatus.NOT_SEND,
command: this.send,
},
{
label: 'استعلام وضعیت',
icon: 'pi pi-info',
neededStatus: TspProviderResponseStatus.FISCAL_QUEUED,
command: this.inquiry,
},
{
label: 'دلایل خطا',
icon: 'pi pi-question',
neededStatus: TspProviderResponseStatus.FAILURE,
command: this.showErrors,
},
{
label: 'ابطال',
icon: 'pi pi-eraser',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showRevokeConfirmation,
},
{
label: 'اصلاح',
icon: 'pi pi-file-edit',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showCorrection,
},
{
label: 'برگشت از فروش',
icon: 'pi pi-cart-minus',
neededStatus: TspProviderResponseStatus.SUCCESS,
command: this.showReturnFromSale,
},
{
label: 'چاپ',
icon: 'pi pi-print',
neededStatus: '',
command: this.printInvoice,
},
];
this.moreActionMenuItems.set( this.moreActionMenuItems.set(
actions.filter( this.actions.filter((action) => action.neededStatus === invoiceStatus)
// @ts-ignore
(action) => !action.neededStatus || action.neededStatus === invoiceStatus
)
); );
} }
} }
@@ -1,25 +1,24 @@
<div class="inline-flex flex-col gap-1 w-full"> <div class="inline-flex w-full flex-col gap-1">
<div class="inline-flex gap-2 items-center w-full"> <div class="inline-flex w-full items-center gap-2">
<span class="text-muted-color text-base font-normal flex-auto grow-0">{{ label }}:</span> <span class="text-muted-color flex-auto grow-0 text-base font-normal">{{ label }}:</span>
<div <div
class="shrink-0 grow max-sm:text-left" class="text-text-color shrink-0 grow text-base font-bold max-sm:text-left"
[ngClass]="{ [ngClass]="{
'text-right': alignment === 'start', 'text-right': alignment === 'start',
'text-center': alignment === 'center', 'text-center': alignment === 'center',
'text-left': alignment === 'end', 'text-left': alignment === 'end',
}" }">
>
<ng-content> <ng-content>
@if (type === "thumbnail") { @if (type === 'thumbnail') {
<div class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 cursor-pointer"> <div class="bg-surface-100 h-20 w-20 cursor-pointer overflow-hidden rounded-2xl">
@if (valueToShow()) { @if (valueToShow()) {
<img [src]="valueToShow()" class="w-full h-full object-cover" /> <img [src]="valueToShow()" class="h-full w-full object-cover" />
} }
</div> </div>
} @else if (valueType() === "tag") { } @else if (valueType() === 'tag') {
<p-tag [value]="valueToShow()?.toString()" [severity]="value ? 'contrast' : 'danger'" /> <p-tag [value]="valueToShow()?.toString()" [severity]="value ? 'contrast' : 'danger'" />
} @else { } @else {
<span class="text-text-color text-base font-bold grow"> {{ valueToShow() }}</span> <span class="text-text-color grow text-base font-bold"> {{ valueToShow() }}</span>
} }
</ng-content> </ng-content>
</div> </div>
@@ -2,7 +2,9 @@
<form [formGroup]="form"> <form [formGroup]="form">
<app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" /> <app-price-input [control]="form.controls.unit_price" name="unit_price" label="مبلغ واحد" />
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" /> <app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
@if (!isCorrection) {
<app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" /> <app-enum-select [control]="form.controls.payload.controls.karat" type="goldKarat" name="karat" />
}
<app-amount-percentage-input <app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.wages_percentage" [percentageControl]="form.controls.payload.controls.wages_percentage"
@@ -15,6 +15,14 @@
[style]="{ display: 'none' }" [style]="{ display: 'none' }"
> >
</ng-template> </ng-template>
<input
#cameraInput
type="file"
class="hidden"
[accept]="accept"
[attr.capture]="accept?.includes('image') ? cameraCapture : null"
(change)="onNativeFileSelected($event)"
/>
<ng-template <ng-template
#content #content
let-files let-files
@@ -35,7 +43,7 @@
icon="pi pi-pencil" icon="pi pi-pencil"
severity="warn" severity="warn"
size="small" size="small"
(click)="chooseCallback()" (click)="openFilePicker()"
></button> ></button>
<button <button
pButton pButton
@@ -49,7 +57,7 @@
</div> </div>
</div> </div>
} @else { } @else {
<div class="flex items-center justify-center flex-col cursor-pointer py-5 w-full" (click)="chooseCallback()"> <div class="flex items-center justify-center flex-col cursor-pointer py-5 w-full" (click)="openFilePicker()">
<i class="pi pi-cloud-upload border-2! rounded-full! p-8! text-4xl! text-muted-color!"></i> <i class="pi pi-cloud-upload border-2! rounded-full! p-8! text-4xl! text-muted-color!"></i>
<p class="mt-6 mb-0">فایل مورد نظر خود را در اینجا رها کنید</p> <p class="mt-6 mb-0">فایل مورد نظر خود را در اینجا رها کنید</p>
</div> </div>
@@ -1,6 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { import {
Component, Component,
ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
@@ -22,6 +23,7 @@ export class SharedUploadFileComponent implements OnChanges, OnDestroy {
@Input() loading: boolean = false; @Input() loading: boolean = false;
@Input() name: string = 'file'; @Input() name: string = 'file';
@Input() accept: string = 'image/*'; @Input() accept: string = 'image/*';
@Input() cameraCapture: 'user' | 'environment' = 'environment';
@Input() maxSize: Maybe<number> = 5 * 1024 * 1024; @Input() maxSize: Maybe<number> = 5 * 1024 * 1024;
@Input() error: Maybe<string> = null; @Input() error: Maybe<string> = null;
@Input() hint: Maybe<string> = null; @Input() hint: Maybe<string> = null;
@@ -30,6 +32,7 @@ export class SharedUploadFileComponent implements OnChanges, OnDestroy {
@Output() onSelect = new EventEmitter<onSelectFileArgs>(); @Output() onSelect = new EventEmitter<onSelectFileArgs>();
@ViewChild(FileUpload) uploader?: FileUpload; @ViewChild(FileUpload) uploader?: FileUpload;
@ViewChild('cameraInput') cameraInput?: ElementRef<HTMLInputElement>;
filePreview: Maybe<string> = null; filePreview: Maybe<string> = null;
private objectUrl: Maybe<string> = null; private objectUrl: Maybe<string> = null;
@@ -63,8 +66,26 @@ export class SharedUploadFileComponent implements OnChanges, OnDestroy {
this.onSelect.emit({ data: payload, file }); this.onSelect.emit({ data: payload, file });
} }
openFilePicker() {
this.cameraInput?.nativeElement.click();
}
onNativeFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) {
return;
}
this.setPreviewFromFile(file);
const payload = new FormData();
payload.append(this.name, file, file.name);
this.onSelect.emit({ data: payload, file });
input.value = '';
}
onEditFile() { onEditFile() {
this.uploader?.choose(); this.openFilePicker();
} }
onDeleteFilePreview() { onDeleteFilePreview() {
@@ -2,7 +2,7 @@
<input pInputText readonly [name]="name" [value]="valueToShow" class="w-full" /> <input pInputText readonly [name]="name" [value]="valueToShow" class="w-full" />
</uikit-field> </uikit-field>
<p-popover #op appendTo="body"> <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 #wrapperRef class="dir-rtl border-divider bg-background-paper w-full min-w-[320px] overflow-auto rounded-md">
<div class="bg-primary-main sticky top-0 flex justify-center px-3"> <div class="bg-primary-main sticky top-0 flex justify-center px-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="flex h-8 items-center"> <div class="flex h-8 items-center">
+2 -2
View File
@@ -3,7 +3,7 @@
} }
.listKeyValue { .listKeyValue {
@apply grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center; @apply grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3;
} }
.cardShadow { .cardShadow {
@@ -11,7 +11,7 @@
} }
.summaryList { .summaryList {
@apply max-h-125 flex; @apply flex max-h-125;
> * { > * {
@apply w-full; @apply w-full;
} }
-6
View File
@@ -13,12 +13,6 @@ a {
text-decoration: none; text-decoration: none;
} }
.layout-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
}
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
font-size: 14px; font-size: 14px;
+1 -17
View File
@@ -1,23 +1,7 @@
.layout-main-container { .layout-main-container {
display: flex; // padding: 2rem 1rem 2rem 2rem;
flex-direction: column;
// height: 100vh;
// overflow: hidden;
justify-content: space-between;
padding: 2rem 1rem 2rem 2rem;
transition: margin-inline-start var(--layout-section-transition-duration); transition: margin-inline-start var(--layout-section-transition-duration);
&.hideMenu { &.hideMenu {
margin: 0 !important;
transform: translateX(0) !important; transform: translateX(0) !important;
padding-inline-start: 2rem !important;
}
&.isFullPage {
padding-inline-start: 0 !important;
padding: 0 !important;
} }
} }
.layout-main {
flex: 1 1 auto;
// overflow: auto;
}
+7
View File
@@ -0,0 +1,7 @@
.p-inputtext {
box-shadow: none !important;
}
* {
box-shadow: none !important;
}
+3 -2
View File
@@ -1,9 +1,10 @@
// 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://192.168.0.162:5002',
// apiBaseUrl: 'http://localhost:5002',
host: 'localhost', host: 'localhost',
port: 5000, port: 5000,
enableLogging: false, enableLogging: false,