update saleinvoice single and create return from sale form, update agent.md file,
This commit is contained in:
@@ -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…”
|
|
||||||
- “I’m 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.
|
||||||
|
|||||||
@@ -42,13 +42,7 @@
|
|||||||
<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">
|
<div class="flex flex-col gap-4">
|
||||||
<app-datepicker
|
<field-invoice-date [control]="invoiceDate" alignment="horizontal" (onChangeDate)="changeDate($event)" />
|
||||||
label="تاریخ صورتحساب"
|
|
||||||
alignment="horizontal"
|
|
||||||
[min]="invoiceMinDate"
|
|
||||||
[max]="invoiceMaxDate"
|
|
||||||
[control]="invoiceDate"
|
|
||||||
(valueChange)="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>
|
||||||
|
|||||||
@@ -1,8 +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 { MiniMonthlyCalendarComponent } from '@/uikit';
|
import { FieldInvoiceDateComponent } from '@/shared/components/fields/invoice_date.component';
|
||||||
import { formatGregorian, gregorianAddUnit } from '@/utils';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -35,7 +34,8 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
|
|||||||
PosOrderCustomerDialogComponent,
|
PosOrderCustomerDialogComponent,
|
||||||
KeyValueComponent,
|
KeyValueComponent,
|
||||||
Card,
|
Card,
|
||||||
MiniMonthlyCalendarComponent,
|
|
||||||
|
FieldInvoiceDateComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PosOrderSectionComponent {
|
export class PosOrderSectionComponent {
|
||||||
@@ -46,8 +46,6 @@ export class PosOrderSectionComponent {
|
|||||||
placeholderImage = images.placeholders.default;
|
placeholderImage = images.placeholders.default;
|
||||||
|
|
||||||
isVisibleCustomerForm = signal(false);
|
isVisibleCustomerForm = signal(false);
|
||||||
invoiceMinDate = formatGregorian(gregorianAddUnit(new Date(), -7, 'days'));
|
|
||||||
invoiceMaxDate = formatGregorian(new Date());
|
|
||||||
|
|
||||||
invoiceDate = new FormControl(this.store.invoiceDate());
|
invoiceDate = new FormControl(this.store.invoiceDate());
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,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;
|
||||||
@@ -30,7 +30,7 @@ export interface ISaleInvoiceFullRawResponse {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -64,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;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<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.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" />
|
||||||
<app-key-value label="نوع صورتحساب">
|
<app-key-value label="نوع صورتحساب">
|
||||||
@@ -109,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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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 { SharedReturnFormComponent } from './returnForm/form.component';
|
||||||
|
|
||||||
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
|
export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos';
|
|||||||
CatalogInvoiceSettlementTypeTagComponent,
|
CatalogInvoiceSettlementTypeTagComponent,
|
||||||
SharedLightBottomsheetComponent,
|
SharedLightBottomsheetComponent,
|
||||||
ProgressSpinner,
|
ProgressSpinner,
|
||||||
|
SharedReturnFormComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SharedSaleInvoiceSingleViewComponent {
|
export class SharedSaleInvoiceSingleViewComponent {
|
||||||
@@ -90,6 +92,8 @@ export class SharedSaleInvoiceSingleViewComponent {
|
|||||||
@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);
|
||||||
@@ -112,16 +116,17 @@ export class SharedSaleInvoiceSingleViewComponent {
|
|||||||
send() {
|
send() {
|
||||||
this.onSendToTsp.emit();
|
this.onSendToTsp.emit();
|
||||||
}
|
}
|
||||||
showRevokeConfirmation() {
|
async showRevokeConfirmation() {
|
||||||
this.confirmationService.ask({
|
await this.confirmationService.ask({
|
||||||
header: `ابطال صورتحساب شماره ${this.invoice!.invoice_number}`,
|
header: `ابطال صورتحساب شماره ${this.invoice!.invoice_number}`,
|
||||||
message: `در صورت تایید ابطال صورتحساب شماره ${this.invoice!.invoice_number} بر روی دکمهی ابطال کلیک کنید.`,
|
message: `در صورت تایید ابطال صورتحساب شماره ${this.invoice!.invoice_number} بر روی دکمهی ابطال کلیک کنید.`,
|
||||||
acceptLabel: 'ابطال',
|
acceptLabel: 'ابطال',
|
||||||
acceptButtonProps: {
|
acceptButtonProps: {
|
||||||
severity: 'dangerdock',
|
severity: 'dangerdock',
|
||||||
},
|
},
|
||||||
|
variant: 'danger',
|
||||||
|
|
||||||
accept: () => {
|
accept: async () => {
|
||||||
// this.deferredInstallPrompt.prompt();
|
// this.deferredInstallPrompt.prompt();
|
||||||
// this.deferredInstallPrompt.userChoice.finally(() => {
|
// this.deferredInstallPrompt.userChoice.finally(() => {
|
||||||
// this.deferredInstallPrompt = null;
|
// this.deferredInstallPrompt = null;
|
||||||
@@ -133,7 +138,9 @@ 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) {
|
||||||
@@ -320,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,10 +42,10 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
CalculatedAmountCardComponent,
|
CalculatedAmountCardComponent,
|
||||||
InputComponent,
|
InputComponent,
|
||||||
} from '@/shared/components';
|
} 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 { IGoldPayload } from '@/shared/models';
|
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';
|
||||||
@@ -15,6 +17,7 @@ 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 goldAmountCalculationUtil from './gold-amount-calculation.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'shared-gold-payload-form',
|
selector: 'shared-gold-payload-form',
|
||||||
@@ -54,18 +57,8 @@ export class SharedGoldPayloadFormComponent extends AbstractForm<
|
|||||||
|
|
||||||
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);
|
||||||
@@ -105,6 +98,7 @@ export class SharedGoldPayloadFormComponent 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],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,31 +134,21 @@ export class SharedGoldPayloadFormComponent 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);
|
||||||
@@ -177,8 +161,7 @@ export class SharedGoldPayloadFormComponent extends AbstractForm<
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './gold-amount-calculation.util';
|
||||||
export * from './gold-form.component';
|
export * from './gold-form.component';
|
||||||
export * from './standard-form.component';
|
export * from './standard-form.component';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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-xs overflow-auto rounded-md">
|
<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="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">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<i class="pi pi-chevron-right cursor-pointer" (click)="changeMonth(-1)"></i>
|
<i class="pi pi-chevron-right cursor-pointer" (click)="changeMonth(-1)"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-32 items-center justify-center px-3 font-bold select-none">
|
<div class="flex w-32 items-center justify-center px-3 text-lg font-bold select-none">
|
||||||
{{ monthInfo().monthTitle }}
|
{{ monthInfo().monthTitle }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
[class.bg-background-default]="closedDays.includes($index)">
|
[class.bg-background-default]="closedDays.includes($index)">
|
||||||
@if (!(day.isNextMonth || day.isPastMonth)) {
|
@if (!(day.isNextMonth || day.isPastMonth)) {
|
||||||
<span
|
<span
|
||||||
class="flex aspect-square w-full cursor-pointer items-center justify-center rounded px-2 py-1 transition-colors select-none"
|
class="flex aspect-square w-full cursor-pointer items-center justify-center rounded px-2 py-1 text-lg transition-colors select-none"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'bg-surface-ground font-bold': day.isCurrentDay,
|
'bg-surface-ground font-bold': day.isCurrentDay,
|
||||||
'bg-primary! text-primary-contrast!': isSelected(day),
|
'bg-primary! text-primary-contrast!': isSelected(day),
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import { Component, EventEmitter, Input, Output, ViewChild, computed, signal } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import jalaliday from 'jalaliday';
|
import jalaliday from 'jalaliday';
|
||||||
|
|
||||||
import { Maybe } from '@/core';
|
|
||||||
import { formatJalali } from '@/utils';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { InputText } from 'primeng/inputtext';
|
import { InputText } from 'primeng/inputtext';
|
||||||
import { Popover } from 'primeng/popover';
|
import { Popover } from 'primeng/popover';
|
||||||
|
|
||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { formatJalali } from '@/utils';
|
||||||
|
|
||||||
import { UikitFieldComponent } from '../uikit-field.component';
|
import { UikitFieldComponent } from '../uikit-field.component';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MonthInfo,
|
MonthInfo,
|
||||||
MonthlyWeekDate,
|
MonthlyWeekDate,
|
||||||
@@ -25,145 +37,129 @@ dayjs.extend(jalaliday);
|
|||||||
selector: 'app-datepicker',
|
selector: 'app-datepicker',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './datepicker.component.html',
|
templateUrl: './datepicker.component.html',
|
||||||
imports: [Popover, UikitFieldComponent, InputText, CommonModule],
|
imports: [CommonModule, Popover, InputText, UikitFieldComponent],
|
||||||
})
|
})
|
||||||
export class MiniMonthlyCalendarComponent {
|
export class DatepickerComponent {
|
||||||
@Input() control!: FormControl<Maybe<string>>;
|
@Input({ required: true })
|
||||||
@Input() alignment?: 'vertical' | 'horizontal' = 'vertical';
|
control!: FormControl<Maybe<string>>;
|
||||||
@Input() name?: string = 'date';
|
|
||||||
@Input() label?: string = 'تاریخ';
|
|
||||||
@Input() closeOnSelect?: boolean = true;
|
|
||||||
@Input() min?: string;
|
|
||||||
@Input() max?: string;
|
|
||||||
@Input() closedDays: number[] = [];
|
|
||||||
|
|
||||||
@Output() clickOnDayEvent = new EventEmitter<MonthlyWeekDate>();
|
@Input()
|
||||||
@Output() valueChange = new EventEmitter<string>();
|
alignment: 'vertical' | 'horizontal' = 'vertical';
|
||||||
|
|
||||||
@ViewChild('op') op!: Popover;
|
@Input()
|
||||||
|
name = 'date';
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
label = 'تاریخ';
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
closeOnSelect = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gregorian YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
min?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gregorian YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
max?: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
closedDays: number[] = [];
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
valueChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
clickOnDayEvent = new EventEmitter<MonthlyWeekDate>();
|
||||||
|
|
||||||
|
@ViewChild('op')
|
||||||
|
op!: Popover;
|
||||||
|
|
||||||
readonly daysName = daysName;
|
readonly daysName = daysName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current displayed month
|
* Currently displayed month
|
||||||
* Gregorian YYYY-MM-DD
|
|
||||||
*/
|
*/
|
||||||
private readonly activeDateSignal = signal(dayjs().format('YYYY-MM-DD'));
|
private readonly activeDateSignal = signal(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
/**
|
|
||||||
* Selected value
|
|
||||||
* Gregorian YYYY-MM-DD
|
|
||||||
*/
|
|
||||||
private readonly selectedDateSignal = signal<Maybe<string>>(null);
|
|
||||||
|
|
||||||
disabled = false;
|
|
||||||
|
|
||||||
monthInfo = computed<MonthInfo>(() => prepareMonthInfo(this.activeDateSignal()));
|
monthInfo = computed<MonthInfo>(() => prepareMonthInfo(this.activeDateSignal()));
|
||||||
|
|
||||||
weekDaysDates = computed(() => prepareWeekDayDates(this.monthInfo(), this.min, this.max));
|
weekDaysDates = computed(() => prepareWeekDayDates(this.monthInfo(), this.min, this.max));
|
||||||
|
|
||||||
// -----------------------
|
constructor() {
|
||||||
// Getters
|
effect(() => {
|
||||||
// -----------------------
|
const value = this.value;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.activeDateSignal.set(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.control?.value) {
|
||||||
|
this.activeDateSignal.set(this.control.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.control.valueChanges.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
this.activeDateSignal.set(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get value(): Maybe<string> {
|
get value(): Maybe<string> {
|
||||||
return this.selectedDateSignal();
|
return this.control?.value ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get valueToShow(): Maybe<string> {
|
get valueToShow(): Maybe<string> {
|
||||||
return this.selectedDateSignal() && formatJalali(this.selectedDateSignal()!);
|
if (!this.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatJalali(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeDate(): string {
|
get activeDate(): string {
|
||||||
return this.activeDateSignal();
|
return this.activeDateSignal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// Setters
|
|
||||||
// -----------------------
|
|
||||||
|
|
||||||
set value(date: Maybe<string>) {
|
|
||||||
this.selectedDateSignal.set(date);
|
|
||||||
this.control.setValue(date);
|
|
||||||
|
|
||||||
if (date) {
|
|
||||||
this.activeDateSignal.set(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set activeDate(date: string) {
|
set activeDate(date: string) {
|
||||||
this.activeDateSignal.set(date);
|
this.activeDateSignal.set(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// Navigation
|
|
||||||
// -----------------------
|
|
||||||
|
|
||||||
changeMonth(months: number): void {
|
changeMonth(months: number): void {
|
||||||
const newDate = dayjs(this.activeDateSignal()).add(months, 'month').format('YYYY-MM-DD');
|
this.activeDateSignal.set(
|
||||||
|
dayjs(this.activeDateSignal()).add(months, 'month').format('YYYY-MM-DD')
|
||||||
this.activeDateSignal.set(newDate);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// Selection
|
|
||||||
// -----------------------
|
|
||||||
|
|
||||||
selectDate(day: MonthlyWeekDate): void {
|
selectDate(day: MonthlyWeekDate): void {
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (day.isDisabled) {
|
if (day.isDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.value = day.gregorianDate;
|
this.control.setValue(day.gregorianDate);
|
||||||
|
|
||||||
this.onChange(day.gregorianDate);
|
this.control.markAsDirty();
|
||||||
|
this.control.markAsTouched();
|
||||||
|
|
||||||
this.onTouched();
|
this.valueChange.emit(day.gregorianDate);
|
||||||
|
this.clickOnDayEvent.emit(day);
|
||||||
|
|
||||||
if (this.closeOnSelect) {
|
if (this.closeOnSelect) {
|
||||||
this.op.toggle(false);
|
this.op.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clickOnDayEvent.emit(day);
|
|
||||||
this.valueChange.emit(day.gregorianDate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(day: MonthlyWeekDate): boolean {
|
isSelected(day: MonthlyWeekDate): boolean {
|
||||||
return !!this.value && this.value === day.gregorianDate;
|
return this.value === day.gregorianDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// CVA
|
|
||||||
// -----------------------
|
|
||||||
|
|
||||||
private onChange: (value: Maybe<string>) => void = () => {};
|
|
||||||
|
|
||||||
private onTouched: () => void = () => {};
|
|
||||||
|
|
||||||
writeValue(value: Maybe<string>): void {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerOnChange(fn: (value: Maybe<string>) => void): void {
|
|
||||||
this.onChange = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerOnTouched(fn: () => void): void {
|
|
||||||
this.onTouched = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean): void {
|
|
||||||
this.disabled = isDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// Helpers
|
|
||||||
// -----------------------
|
|
||||||
|
|
||||||
trackByIndex(index: number): number {
|
trackByIndex(index: number): number {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user