create light bottomsheet drawer for gold payload

This commit is contained in:
2026-05-13 10:39:43 +03:30
parent ebd2aa46dc
commit d9e74da0e2
9 changed files with 216 additions and 84 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
{ {
"$schema": "./node_modules/@angular/service-worker/config/schema.json", "$schema": "./node_modules/@angular/service-worker/config/schema.json",
"appData": { "appData": {
"appVersion": "0.0.4", "appVersion": "0.0.9",
"buildDate": "2026-05-11T16:11:59.783Z", "buildDate": "2026-05-13T06:42:17.700Z",
"buildNumber": 4 "buildNumber": 9
}, },
"assetGroups": [ "assetGroups": [
{ {
@@ -1,8 +1,8 @@
<shared-dialog <shared-light-bottomsheet
[(visible)]="visible" [(visible)]="visible"
[header]="preparedTitle()" [header]="preparedTitle()"
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true" [closable]="true"
(onHide)="close()" (onHide)="close()"
> >
@@ -18,4 +18,4 @@
/> />
} }
} }
</shared-dialog> </shared-light-bottomsheet>
@@ -1,6 +1,6 @@
import { IGoodResponse } from '@/domains/pos/models/good.io'; import { IGoodResponse } from '@/domains/pos/models/good.io';
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog'; import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
import { SharedDialogComponent } from '@/shared/components/dialog/dialog.component'; import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core'; import { Component, computed, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { IGoldPayload, IPosOrderItem, IStandardPayload, TPosOrderGoodPayload } from '../models'; import { IGoldPayload, IPosOrderItem, IStandardPayload, TPosOrderGoodPayload } from '../models';
import { PosLandingStore } from '../store/main.store'; import { PosLandingStore } from '../store/main.store';
@@ -10,7 +10,7 @@ import { PosStandardPayloadFormComponent } from './payloads/standard/form.compon
@Component({ @Component({
selector: 'pos-payload-form-dialog', selector: 'pos-payload-form-dialog',
templateUrl: './payload-form.component.html', templateUrl: './payload-form.component.html',
imports: [PosGoldPayloadFormComponent, SharedDialogComponent, PosStandardPayloadFormComponent], imports: [PosGoldPayloadFormComponent, SharedLightBottomsheetComponent, PosStandardPayloadFormComponent],
}) })
export class PayloadFormDialogComponent extends AbstractDialog { export class PayloadFormDialogComponent extends AbstractDialog {
@Input({ required: true }) good!: IGoodResponse; @Input({ required: true }) good!: IGoodResponse;
@@ -1,69 +1,67 @@
@if (formVisible()) { <div class="form-group">
<div class="form-group"> <form [formGroup]="form">
<form [formGroup]="form"> <app-input [control]="form.controls.unit_price" name="unit_price" label="قیمت پایه هر گرم" type="price" />
<app-input [control]="form.controls.unit_price" name="unit_price" label="قیمت پایه هر گرم" type="price" /> <app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" />
<app-input [control]="form.controls.quantity" name="quantity" label="مقدار" type="number" suffix="گرم" /> <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
[percentageControl]="form.controls.payload.controls.wages_percentage"
[amountControl]="form.controls.payload.controls.wages_amount"
[baseAmount]="unitWithQuantity()"
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="wages"
label="اجرت"
/>
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.commission_percentage"
[amountControl]="form.controls.payload.controls.commission_amount"
[baseAmount]="unitWithQuantity()"
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="commission"
label="حق‌العمل"
/>
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.profit_percentage"
[amountControl]="form.controls.payload.controls.profit_amount"
[baseAmount]="totalAmountBeforeProfit()"
[minAmount]="0"
[maxAmount]="totalAmountBeforeProfit()"
name="profit"
label="سود"
/>
</form>
<app-amount-percentage-input <app-amount-percentage-input
[percentageControl]="form.controls.discount_percentage" [percentageControl]="form.controls.payload.controls.wages_percentage"
[amountControl]="form.controls.discount_amount" [amountControl]="form.controls.payload.controls.wages_amount"
[baseAmount]="baseAmountForDiscountCalculation()" [baseAmount]="unitWithQuantity()"
[minAmount]="0" [minAmount]="0"
[maxAmount]="baseAmountForDiscountCalculation()" [maxAmount]="unitWithQuantity()"
name="discount" name="wages"
label=خفیف" label="اجرت"
> />
<ng-template #labelSuffix> <app-amount-percentage-input
<p-selectButton [percentageControl]="form.controls.payload.controls.commission_percentage"
[options]="discountTypeItems" [amountControl]="form.controls.payload.controls.commission_amount"
[(ngModel)]="discountType" [baseAmount]="unitWithQuantity()"
[allowEmpty]="false" [minAmount]="0"
optionLabel="label" [maxAmount]="unitWithQuantity()"
optionValue="value" name="commission"
(onChange)="changeDiscountCalculation($event)" label="حق‌العمل"
/> />
</ng-template> <app-amount-percentage-input
</app-amount-percentage-input> [percentageControl]="form.controls.payload.controls.profit_percentage"
[amountControl]="form.controls.payload.controls.profit_amount"
[baseAmount]="totalAmountBeforeProfit()"
[minAmount]="0"
[maxAmount]="totalAmountBeforeProfit()"
name="profit"
label="سود"
/>
</form>
<hr /> <app-amount-percentage-input
<div class="flex flex-col gap-4 justify-center w-full"> [percentageControl]="form.controls.discount_percentage"
<pos-form-dialog-amount-card-template [amountControl]="form.controls.discount_amount"
[totalAmount]="totalAmount()" [baseAmount]="baseAmountForDiscountCalculation()"
[baseTotalAmount]="baseTotalAmount()" [minAmount]="0"
[discountAmount]="form.controls.discount_amount.value || 0" [maxAmount]="baseAmountForDiscountCalculation()"
[taxAmount]="taxAmount()" name="discount"
label="تخفیف"
>
<ng-template #labelSuffix>
<p-selectButton
[options]="discountTypeItems"
[(ngModel)]="discountType"
[allowEmpty]="false"
optionLabel="label"
optionValue="value"
(onChange)="changeDiscountCalculation($event)"
/> />
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button> </ng-template>
</div> </app-amount-percentage-input>
<hr />
<div class="flex flex-col gap-4 justify-center w-full">
<pos-form-dialog-amount-card-template
[totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()"
[discountAmount]="form.controls.discount_amount.value || 0"
[taxAmount]="taxAmount()"
/>
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
</div> </div>
} </div>
@@ -52,7 +52,6 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
baseAmountForDiscountCalculation = signal<number>(0); baseAmountForDiscountCalculation = signal<number>(0);
taxAmount = signal<number>(0); taxAmount = signal<number>(0);
totalAmount = signal<number>(0); totalAmount = signal<number>(0);
formVisible = signal<boolean>(false);
preparedCTAText = computed(() => (this.editMode ? 'اعمال ویرایش' : 'افزودن به لیست خرید')); preparedCTAText = computed(() => (this.editMode ? 'اعمال ویرایش' : 'افزودن به لیست خرید'));
@@ -95,16 +94,6 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
form = this.initialForm(); form = this.initialForm();
override ngAfterViewInit(): void {
setTimeout(() => {
this.formVisible.set(true);
if (this.editMode) {
this.form.patchValue(this.initialValues || {}, { emitEvent: false });
this.updateCalculateAmount(this.form.value as any);
}
}, 200);
}
override submitForm(payload: IPosOrderItem) { override submitForm(payload: IPosOrderItem) {
this.onSubmit.emit({ this.onSubmit.emit({
...payload, ...payload,
@@ -19,7 +19,7 @@
type="button" type="button"
pButton pButton
class="sticky! bottom-0 inset-x-0 w-full right-0 z-1200 border-t border-surface-border bg-surface-0 px-4 py-3 shadow-[0_-4px_16px_rgba(0,0,0,0.08)] md:hidden! rounded-b-none!" class="sticky! bottom-0 inset-x-0 w-full right-0 z-1200 border-t border-surface-border bg-surface-0 px-4 py-3 shadow-[0_-4px_16px_rgba(0,0,0,0.08)] md:hidden! rounded-b-none!"
[style.paddingBottom]="'calc(0.75rem + env(safe-area-inset-bottom))'" [style.paddingBottom]="'calc(0.75rem + {{env(safe-area-inset-bottom)}})'"
(click)="openInvoiceBottomSheet()" (click)="openInvoiceBottomSheet()"
> >
<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">
@@ -36,7 +36,7 @@ export abstract class AbstractForm<
return payload; return payload;
} }
ngAfterViewInit() { ngOnInit() {
this.form.reset(this.initialValues ?? this.defaultValues); this.form.reset(this.initialValues ?? this.defaultValues);
// Object.entries(this.form.controls).forEach(([key, control]) => { // Object.entries(this.form.controls).forEach(([key, control]) => {
// console.log(key, control); // console.log(key, control);
@@ -0,0 +1,25 @@
<div class="light-bottomsheet-mask" (click)="onMaskClick()"></div>
<section
class="light-bottomsheet-panel"
role="dialog"
aria-modal="true"
[attr.aria-hidden]="!visible"
[ngStyle]="style || { 'max-height': mobileDrawerHeight, height: 'auto' }"
>
<div class="light-bottomsheet-content">
@if (header || closable) {
<header class="light-bottomsheet-header">
<h3 class="light-bottomsheet-title">{{ header }}</h3>
@if (closable) {
<button type="button" class="light-bottomsheet-close" (click)="onVisibilityChange(false)" aria-label="Close">
×
</button>
}
</header>
}
<div class="p-4">
<ng-content></ng-content>
</div>
</div>
</section>
@@ -0,0 +1,120 @@
import { NgStyle } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'shared-light-bottomsheet',
templateUrl: './light-bottomsheet.component.html',
imports: [NgStyle],
styles: [
`
:host {
position: fixed;
inset: 0;
z-index: 11020;
}
.light-bottomsheet-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.22);
will-change: opacity;
animation: sheetFadeIn var(--sheet-transition, 130ms cubic-bezier(0.2, 0, 0, 1));
}
.light-bottomsheet-panel {
position: absolute;
right: 0;
bottom: 0;
left: 0;
max-height: 80svh;
background: var(--surface-card, #fff);
border-radius: 12px 12px 0 0;
box-shadow: none;
transform: translate3d(0, 0, 0);
will-change: transform;
backface-visibility: hidden;
animation: sheetSlideUp var(--sheet-transition, 130ms cubic-bezier(0.2, 0, 0, 1));
overflow: hidden;
}
.light-bottomsheet-content {
max-height: 80svh;
overflow: auto;
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
.light-bottomsheet-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
}
.light-bottomsheet-title {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
}
.light-bottomsheet-close {
border: 0;
background: transparent;
padding: 0.25rem;
line-height: 1;
font-size: 1.25rem;
}
@keyframes sheetSlideUp {
from {
transform: translate3d(0, 100%, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes sheetFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`,
],
host: {
'[style.display]': 'visible ? "block" : "none"',
'[style.--sheet-transition]': 'transitionOptions',
},
})
export class SharedLightBottomsheetComponent {
@Input() header = '';
@Input() visible = false;
@Output() visibleChange = new EventEmitter<boolean>();
@Input() modal = true;
@Input() closable = true;
@Input() style: Record<string, string> | undefined;
@Input() mobileDrawerHeight = '80svh';
@Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)';
@Output() onHide = new EventEmitter<any>();
onVisibilityChange(nextValue: boolean) {
this.visible = nextValue;
this.visibleChange.emit(nextValue);
if (!nextValue) {
this.onHide.emit();
}
}
onMaskClick() {
if (this.modal && this.closable) {
this.onVisibilityChange(false);
}
}
}