feat: enhance form components with validation and dynamic properties; update payment handling and UI interactions

This commit is contained in:
2026-05-24 19:40:02 +03:30
parent cdd2bd6bee
commit eb671d5949
15 changed files with 283 additions and 865 deletions
+169 -804
View File
@@ -1,98 +1,188 @@
# AGENT.md
## Purpose
## Scope
- This file defines repository-specific instructions for coding agents.
- Scope is the full repo unless a deeper `AGENT.md` overrides it.
- Use RTK to reduce token usage, avoid repeated context transfer, and preserve repository knowledge across sessions.
Applies to the full repository unless overridden by a deeper `AGENT.md`.
Stack:
- Angular 20
- Standalone components
- pnpm
- Docker
- RTK
---
# EXECUTION RULES (STRICT)
# EXECUTION RULES
Minimize reasoning verbosity.
## Response Style
Do not:
- narrate thoughts
- speculate
- explain obvious steps
- describe intentions before acting
- write reflective/internal commentary
Avoid messages like:
- "I think I need to..."
- "I should inspect..."
- "I'm going to..."
- "I wonder if..."
- "Let me check..."
- "I'm curious about..."
Instead:
- act immediately
- use short operational updates only when necessary
Good:
Inspecting environment files and Angular configs.
Bad:
I think I should inspect the environment files first to understand how everything is connected before deciding what changes are necessary.
---
# 🔇 MINIMAL PLANNING MODE
Only create plans when:
- task has multiple independent phases
- task is ambiguous
- task is large/refactor-level
Do not create plans for:
- small fixes
- single-file edits
- obvious tasks
- Keep responses short.
- Do not narrate thoughts.
- Do not explain obvious steps.
- Do not create plans for simple tasks.
- Prefer implementation over exploration.
Avoid:
- repeated plan updates
- verbose planning
- unnecessary decomposition
---
# ✂️ MINIMAL OUTPUT RULES
Keep responses compact.
Do not:
- restate the user request
- explain already-visible edits
- summarize trivial findings
- narrate file discovery
- explain failed reads unless blocking
- describe obvious implementation details
- “I think…”
- “Let me check…”
- “I should inspect…”
- “Im going to…”
Prefer:
Updated environment files for development, staging, production, and tis.
Avoid:
I found a mismatch and then investigated several files before determining that...
- “Updated environment files.”
- “Applied tenant config fix.”
---
# 📦 FINAL RESPONSE RULES
# RTK RULES (MANDATORY)
Final responses must be under 10 lines unless:
Always prefer RTK commands.
- user explicitly asks for explanation
- architectural decisions changed
Use:
- `rtk ls`
- `rtk grep`
- `rtk smart`
- `rtk read`
- `rtk git diff`
- `rtk git status`
Avoid raw:
- `cat`
- `grep`
- `rg`
- `tree`
- `git diff`
- recursive `find`
Use raw shell only for:
- builds
- runtime/debugging
- Docker
- pnpm
- commands RTK cannot perform
---
# FILE READING POLICY
Preferred order:
1. `rtk grep`
2. `rtk smart`
3. `rtk read`
Rules:
- Search before reading.
- Read only required files.
- Do not read TS/HTML/SCSS together unless required.
- Stop exploring once edit location is clear.
- Avoid rereading unchanged files.
Large files:
- `rtk read <file> -l aggressive`
---
# ANGULAR WORKFLOW
For components:
1. `rtk grep "<component-name>"`
2. `rtk smart component.ts`
3. Read template only if UI changes are required
4. Read styles only if styling changes are required
Avoid broad module inspection.
---
# TOKEN RULES
Do not:
- dump large files
- scan unrelated folders
- inspect generated directories
- perform repeated searches
- over-explain edits
Avoid:
- `dist/`
- `.angular/`
- `coverage/`
- `node_modules/`
- `.git/`
---
# CHANGE POLICY
- Keep changes minimal and scoped.
- Reuse existing patterns.
- Avoid unrelated refactors.
- Preserve tenant separation.
- Prefer root-cause fixes.
---
# PROJECT RULES
## Tenant Builds
- `default``dist/production`
- `tis``dist/tis`
Keep Docker `DIST_DIR` aligned with Angular output.
Do not use Angular `fileReplacements` for static assets.
---
## Input Component
File:
- `src/app/shared/components/input/input.component.ts`
For:
- `type="number"`
- `type="price"`
Requirements:
- normalize Persian/Arabic digits
- allow only digits and `.`
- support fixed precision
- keep identifier fields string-safe
---
## Shared Field Rules
When creating fields:
- use shared field wrappers
- reuse `app-input`
- register exports in shared indexes
- keep control names consistent
---
# VALIDATION
TypeScript:
- `pnpm -s exec tsc -p tsconfig.app.json --noEmit`
Docker:
- `docker compose build app_default`
- `docker compose build app_tis`
Validate only impacted areas when possible.
---
# FINAL RESPONSE RULES
Keep final responses under 10 lines unless:
- validation failed
- multiple systems were affected
- architecture changed
- user requested explanation
Prefer:
@@ -101,729 +191,4 @@ Updated:
- file2
Validation:
- pnpm tsc passed
Avoid:
- long prose summaries
- unnecessary explanations
- repeating repository context
---
# 🚫 NO TEACHING MODE
Do not explain:
- Angular basics
- TypeScript basics
- RxJS basics
- obvious framework behavior
- standard programming concepts
Assume repository maintainers already understand the stack.
---
# 🧠 CONTEXT PRESERVATION RULES
Avoid repeating previously established context.
Do not repeatedly mention:
- repository structure
- framework details
- previously inspected files
- known architecture
- already-established conventions
Assume prior context remains valid unless changed.
---
# 🛑 EXPLORATION STOP RULE
If the likely edit location is identified:
- stop searching
- stop exploring
- implement changes
Do not continue repository exploration after:
- target component found
- target service found
- target config found
- target store found
- target route found
---
# 🎯 EDIT-FIRST BEHAVIOR
After identifying the target location:
- edit quickly
- avoid excessive inspection
- avoid repeated verification reads
- avoid speculative exploration
Prefer implementation over exploration.
---
# ✍️ EDIT CONFIDENCE RULES
Prefer direct implementation when:
- existing patterns are obvious
- nearby components establish conventions
- requested change is localized
- existing abstractions already match requirements
Do not over-investigate obvious implementations.
---
# ⚠️ FAILURE HANDLING RULES
If a file read fails:
- do not retry repeatedly
- verify path once
- continue with nearest valid target
Do not:
- repeatedly attempt missing files
- scan nearby directories unnecessarily
- speculate about missing files
- retry identical commands
---
# 📏 HARD CONTEXT LIMITS
Do not:
- read more than 2 files before first edit
- read more than 1 sibling file unless required
- reread unchanged files
- inspect unrelated modules
For small tasks:
- maximum 3 repository reads before editing
For medium tasks:
- maximum 8 repository reads before editing
Large/refactor tasks may exceed limits only when necessary.
---
# 🚫 AVOID MULTI-FILE EAGER READS
Never chain multiple reads in one command unless necessary.
Avoid:
- rtk read a.ts && rtk read b.ts && rtk read c.ts
Prefer sequential targeted reads.
---
# ✅ RTK-FIRST RULES (STRICT)
Always prefer RTK commands for repository inspection, navigation, and code understanding.
## Command Rewrite Rules
Prefer:
- git status -> rtk git status
- git diff -> rtk git diff
- git log -> rtk git log
- ls -> rtk ls
- tree -> rtk ls
- cat <file> -> rtk read <file>
- grep <pattern> -> rtk grep <pattern>
- rg <pattern> -> rtk grep <pattern>
- find -> rtk find
Avoid raw commands for repository inspection when RTK equivalents exist.
Do not:
- use raw `cat` for large files
- use raw `grep`/`rg` across the repository
- use recursive `find` blindly
- inspect large diffs with raw `git diff`
- scan directories manually before searching
Raw commands are allowed only when:
- RTK cannot perform the operation
- executing builds/tests/runtime commands
- debugging environment/runtime issues
- working outside indexed repository scope
---
# 🧩 FILE READING RULES (IMPORTANT)
Prefer minimal-context reads.
## Preferred Order
1. `rtk grep`
2. `rtk smart`
3. `rtk read`
4. aggressive read only if required
## Rules
Never immediately read a full file after search.
Before reading:
- identify exact symbol/component/function
- inspect only relevant sections
Prefer:
- rtk smart <file>
before:
- rtk read <file>
For large files:
- rtk read <file> -l aggressive
Only when necessary.
Avoid:
- reading TS + HTML + SCSS together
- opening sibling files preemptively
- rereading files already inspected
- reading generated or unrelated files
---
## Angular-Specific Strategy
For Angular components:
1. grep component selector/class
2. smart-read TS file
3. read template only if UI changes required
4. read styles only if styling changes required
Example:
bash
rtk grep "invoice-type-card"
rtk smart invoice-type-card.component.ts
rtk read invoice-type-card.component.html
Avoid:
bash
rtk read component.ts
rtk read component.html
rtk read component.scss
unless all files are actually needed.
# 📂 CODE NAVIGATION WORKFLOW (MANDATORY)
Follow this order when working in the repository.
## 1. Discover Structure
bash
rtk ls
## 2. Search Before Opening Files
bash
rtk grep <symbol | class | function | DTO | service>
Examples:
bash
rtk grep "InputComponent"
rtk grep "fieldControl"
rtk grep "breadcrumbItems"
## 3. Read Only Necessary Files
bash
rtk read <file>
## 4. Large File Strategy
For files larger than ~300 lines:
bash
rtk read <file> -l aggressive
## 5. Fast Context Understanding
bash
rtk smart <file>
Never:
- open many files blindly
- read entire modules without searching first
- inspect unrelated folders
- load generated directories into context
---
# 🧠 DIFF INSPECTION RULES
For repository changes use:
bash
rtk git diff
For large diffs:
bash
rtk git diff -l aggressive
Avoid raw:
bash
git diff
unless RTK diff is unavailable.
---
# 🚫 LOW-VALUE PATHS
Avoid loading:
dist/
.angular/
coverage/
node_modules/
.git/
.cache/
.tmp/
unless explicitly required.
---
# RTK Usage Rules
## Required RTK Usage
- Always use RTK for:
- repository indexing
- semantic code search
- symbol lookup
- dependency tracing
- architectural context
- change impact analysis
- Prefer RTK context retrieval over:
- reading large files entirely
- repeatedly scanning unchanged directories
- re-sending full file contents
- broad grep operations
- Retrieve only the minimal relevant context before making changes.
## Recommended RTK Workflow
1. Index repository
2. Search related symbols/files/components
3. Retrieve minimal context
4. Apply scoped changes
5. Validate impacted areas only
Example workflow:
bash
rtk index
rtk search "InputComponent number normalization"
rtk search "fieldControl"
rtk refs "goodListConfig"
## Token Reduction Rules
- Never load entire large files unless required.
- Never inspect generated folders (`dist`, `.angular`, coverage, etc.).
- Prefer symbol-level retrieval over file-level retrieval.
- Reuse previously retrieved RTK context whenever possible.
- Avoid duplicate searches for the same symbols within one task.
- Use targeted searches:
- component name
- selector
- signal/store name
- config constant
- Angular route
- injected service
- form control key
## Suggested RTK Queries
### Angular Components
bash
rtk search "selector: 'field-"
rtk search "standalone: true"
rtk search "InputComponent"
### Forms & Controls
bash
rtk search "fieldControl."
rtk search "ControlConfig"
rtk search "Validators.required"
### Stores & Signals
bash
rtk search "breadcrumbItems"
rtk search "computed("
rtk search "signal("
### Tenant / Docker
bash
rtk search "DIST_DIR"
rtk search "configuration tis"
rtk search "prebuild:tis"
### List Configs
bash
rtk search "IListConfig"
rtk search "columns:"
rtk search "goodListConfig"
---
## Stack Context
- Frontend: Angular 20 standalone app.
- Package manager: `pnpm`.
- Deployment: Docker / Docker Compose with tenant-specific services.
- AI-assisted repository navigation: RTK.
### Current service mapping expectation
- `app_default` on host port `8090`
- `app_tis` on host port `8091`
---
## Tenant Build Rules
- `default` tenant currently builds via `ng build` and outputs to `dist/production`.
- `tis` tenant builds via `ng build --configuration tis` and outputs to `dist/tis`.
- `prebuild:tis` is tenant-scoped and should keep using scripts under `scripts/tis/*`.
- Keep Docker `DIST_DIR` aligned with actual Angular output path.
- Do not assume `default` Angular configuration is usable unless verified (it may reference missing replacements).
- Do not use Angular `fileReplacements` for static assets (`.png`, `.jpg`, etc.); use tenant public assets or prebuild copy scripts.
---
## Tenant PWA Rules
- Keep tenant manifest files under tenant public assets (e.g. `public-tis/favicon/site.webmanifest`).
- Ensure `manifest` `id`, `start_url`, and `scope` match actual deployment path:
- root deploy: `/`
- subpath deploy example: `/tis/`
- Keep `<link rel="manifest">` path and branding config `manifestPath` aligned.
---
## Input Component Rules
File:
- `src/app/shared/components/input/input.component.ts`
For:
- `type === 'number'`
- `type === 'price'`
Requirements:
- Normalize Persian/Arabic digits to English digits.
- Allow only digits and `.`.
- Support `fixed` precision formatting when provided.
Additional rule:
- Keep behavior for identifier fields (`mobile`, `phone`, `postalCode`, `nationalId`) string-safe.
---
## Change Policy
- Keep changes minimal and scoped to user request.
- Prefer root-cause fixes over temporary workarounds.
- Avoid unrelated refactors.
- Reuse existing patterns and naming conventions.
- Use RTK to understand existing patterns before introducing new ones.
---
## Do / Don't
### Do
- Follow existing field wrapper style in:
- `src/app/shared/components/fields/*.component.ts`
- Reuse `app-input` and set only required props:
- `type`
- `label`
- `name`
- constraints
- Register every new field in:
- `src/app/shared/components/fields/index.ts`
- `src/app/shared/constants/fields/index.ts`
- Keep control keys consistent across:
- form group
- field component `name`
- `fieldControl` key
- Use RTK searches before creating new abstractions.
### Don't
- Don't add one-off field patterns when an existing field component can be reused.
- Don't use invalid Angular file replacements for directories or empty paths.
- Don't change tenant output directories without updating Docker `DIST_DIR`.
- Don't scan unrelated folders/files when RTK can retrieve exact symbols.
- Don't load full files if RTK symbol extraction is sufficient.
---
## How To Create Form Fields
- Create a wrapper component in:
- `src/app/shared/components/fields`
- Use the same pattern as existing files like:
- `src/app/shared/components/fields/name.component.ts`
- `src/app/shared/components/fields/unit_price.component.ts`
Minimal wrapper shape:
ts
@Component({
selector: 'field-example',
template: `<app-input [label]="label" [control]="control" [name]="name" type="simple" />`,
imports: [ReactiveFormsModule, InputComponent],
})
export class ExampleComponent {
@Input({ required: true }) control = new FormControl<string>('');
@Input() name = 'example';
@Input() label = 'Example';
}
---
## Register New Fields
Export the component from:
- `src/app/shared/components/fields/index.ts`
Add its form control factory in:
- `src/app/shared/constants/fields/index.ts`
`fieldControl` entry shape:
- key must match form control name
- return tuple: `[defaultValue, validators]`
Example:
ts
example: (value = '', isRequired = true): ControlConfig => [
value,
isRequired ? [Validators.required] : [],
],
---
## Using Fields In Forms
- In form group builders, use:
- `fieldControl.<key>(initialValue, isRequired)`
- In templates, render matching wrapper component and pass matching control:
- `<field-example [control]="form.controls.example" />`
- For numeric/price behavior:
- use `app-input`
- use `type="number"` or `type="price"`
- optional `[fixed]`
---
## List Component Configs
Centralized list metadata is stored in:
- `src/app/shared/constants/list-configs/`
Never store configs inside domain modules.
### Structure
Structure by data type, not domain:
- `good-list.const.ts`
- `sku-list.const.ts`
- `category-list.const.ts`
### Requirements
Each config implements `IListConfig`:
- `pageTitle`
- `addNewCtaLabel`
- `emptyPlaceholderTitle`
- `emptyPlaceholderDescription`
- `columns`
### Usage
ts
@Input() header: IColumn[] = goodListConfig.columns;
listConfig = goodListConfig;
### Rules
- Any domain needing a list config imports from:
- `@/shared/constants/list-configs`
- No cross-domain dependencies.
- Do not duplicate configs.
---
## Breadcrumb Usage in Stores & Views
- Entity stores expose `breadcrumbItems` as a computed signal.
- Call `store.breadcrumbItems()` in view components and extend with current page:
ts
setBreadcrumb() {
this.breadcrumbService.setItems([
...this.store.breadcrumbItems(),
{ title: 'Current Page' },
]);
}
- Root page breadcrumbs are set in the store's `getData()` method once entity is loaded.
---
## Validation Checklist
### TypeScript-only changes
bash
pnpm -s exec tsc -p tsconfig.app.json --noEmit
### Docker/build changes
bash
docker compose build app_default
docker compose build app_tis
### Validation Strategy
- Start with targeted validation.
- Run broader checks only if needed.
- Validate only impacted tenants/features when possible.
---
## Communication Expectations
- Report exactly which files changed and why.
- Call out assumptions and discovered config mismatches.
- If validation is blocked:
- explain why
- provide next command
- Mention RTK searches/symbols used when relevant to implementation decisions.
---
## Preferred Investigation Order
Before editing code:
1. RTK symbol search
2. Existing implementation discovery
3. Shared abstraction verification
4. Minimal scoped change
5. Targeted validation
Avoid:
- broad exploratory reads
- repeated file dumping
- unnecessary context expansion
---
## Angular Architecture Preferences
- Prefer standalone components.
- Prefer signals/computed over legacy observable-only state when consistent with current architecture.
- Keep forms typed.
- Keep reusable UI in `shared`.
- Keep domain logic outside presentation components.
- Preserve existing tenant separation model.
---
## Performance & AI Efficiency
- Optimize for:
- minimal token usage
- minimal file reads
- scoped context retrieval
- reusable architectural understanding
Prefer:
- RTK symbol search
- targeted reads
- incremental inspection
Avoid:
- dumping full files
- repeated repository scans
- repeated searches for the same symbols
- unnecessary architectural exploration
- RTK should be the primary repository understanding tool.
- tsc passed
@@ -87,7 +87,7 @@ export class NativeBridgeService {
window.NativeBridge.pay(request.amount, request.id || '');
return { success: true };
} catch (error) {
this.toastService.info({ text: (error as Error).message });
// this.toastService.info({ text: (error as Error).message });
return { success: false, error: (error as Error).message };
}
// const fn = window.NativeBridge.pay(123, 'test');
@@ -11,6 +11,8 @@ export function greaterThanValidator(minValue: number): ValidatorFn {
return null;
}
console.log('v', v);
const numericValue = typeof v === 'number' ? v : Number(v);
if (Number.isNaN(numericValue)) {
return { greaterThan: 'مقدار باید عددی باشد' };
@@ -11,8 +11,7 @@
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="wages"
label="اجرت"
/>
label="اجرت" />
{{ initialValues?.payload?.profit }}
<app-amount-percentage-input
@@ -22,8 +21,7 @@
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="commission"
label="حق‌العمل"
/>
label="حق‌العمل" />
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.profit_percentage"
[amountControl]="form.controls.payload.controls.profit"
@@ -31,8 +29,7 @@
[minAmount]="0"
[maxAmount]="totalAmountBeforeProfit()"
name="profit"
label="سود"
/>
label="سود" />
</form>
<app-amount-percentage-input
@@ -42,8 +39,7 @@
[minAmount]="0"
[maxAmount]="baseAmountForDiscountCalculation()"
name="discount"
label="تخفیف"
>
label="تخفیف">
<ng-template #labelSuffix>
<p-selectButton
[options]="discountTypeItems"
@@ -51,19 +47,17 @@
[allowEmpty]="false"
optionLabel="label"
optionValue="value"
(onChange)="changeDiscountCalculation($event)"
/>
(onChange)="changeDiscountCalculation($event)" />
</ng-template>
</app-amount-percentage-input>
<hr />
<div class="flex flex-col gap-4 justify-center w-full">
<div class="flex w-full flex-col justify-center gap-4">
<pos-form-dialog-amount-card-template
[totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()"
[discountAmount]="form.controls.discount.value || 0"
[taxAmount]="taxAmount()"
/>
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
[taxAmount]="taxAmount()" />
<button pButton class="w-full sm:w-auto" (click)="submit()">{{ preparedCTAText() }}</button>
</div>
</div>
@@ -84,7 +84,7 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
const form = this.fb.group({
unit_price: [
this.initialValues?.unit_price || this.goldDefaultUnitPrice() || 0,
[Validators.required],
[Validators.required, greaterThanValidator(0)],
],
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
discount: [this.initialValues?.discount || 0],
@@ -1,4 +1,5 @@
import ISummary from '@/core/models/summary';
import { greaterThanValidator } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components';
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
@@ -32,8 +33,11 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
private readonly initialForm = () => {
const form = this.fb.group({
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
quantity: [this.initialValues?.quantity || 0, [Validators.required]],
unit_price: [
this.initialValues?.unit_price || 0,
[Validators.required, greaterThanValidator(0)],
],
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
discount_amount: [this.initialValues?.discount || 0, [Validators.min(0)]],
discount_percentage: [0, [Validators.min(0), Validators.max(100)]],
});
@@ -4,8 +4,7 @@
[modal]="true"
[style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true"
(onHide)="close()"
>
(onHide)="close()">
<div class="form-group">
<form [formGroup]="form" (submit)="submit()">
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
@@ -14,6 +13,15 @@
</form>
<div class="form-group">
<div class="flex items-center justify-between gap-3">
<uikit-label> نوع تسویه‌حساب </uikit-label>
<p-selectButton
[options]="settlementTypeItems"
[(ngModel)]="selectedSettlementType"
[allowEmpty]="false"
optionLabel="label"
optionValue="value" />
</div>
<uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps">
<p-select
[options]="payByTerminalSteps()"
@@ -23,8 +31,7 @@
optionValue="value"
[disabled]="!remainedAmount()"
class="w-full"
(onChange)="changePayByTerminalSteps($event.value)"
/>
(onChange)="changePayByTerminalSteps($event.value)" />
</uikit-field>
@for (terminalControl of terminalControls; track $index) {
<app-input
@@ -33,16 +40,14 @@
[label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
type="price"
[max]="terminalsMax()[$index]"
[disabled]="payByTerminalSteps()[$index].payed"
>
[disabled]="payByTerminalSteps()[$index].payed">
<ng-template #suffixTemp>
<button
pButton
size="small"
type="button"
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
(click)="fillTerminalRemained($index)"
>
(click)="fillTerminalRemained($index)">
باقی‌مانده مبلغ
</button>
</ng-template>
@@ -55,27 +60,27 @@
name="cash"
label="نقدی/ کارت‌خوان دیگر/ کارت به کارت"
type="price"
[max]="cashMax()"
>
[max]="cashMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
@if (isGoldGuild()) {
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
}
<app-form-footer-actions
submitLabel="ثبت اطلاعات پرداخت و ادامه"
[loading]="submitOrderLoading()"
(onCancel)="close()"
/>
(onCancel)="close()" />
</form>
</div>
</shared-light-bottomsheet>
@@ -1,16 +1,18 @@
import { Maybe } from '@/core';
import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service';
import { PosInfoStore } from '@/domains/pos/store';
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent, KeyValueComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFieldComponent } from '@/uikit';
import { UikitFieldComponent, UikitLabelComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select';
import { SelectButton } from 'primeng/selectbutton';
import { catchError, throwError } from 'rxjs';
import {
IPayment,
@@ -36,15 +38,35 @@ import { PosLandingStore } from '../../store/main.store';
SharedLightBottomsheetComponent,
Select,
UikitFieldComponent,
SelectButton,
UikitLabelComponent,
],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
})
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
private readonly store = inject(PosLandingStore);
private readonly posInfo = inject(PosInfoStore);
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
private readonly nativeBridgeService = inject(NativeBridgeService);
private readonly toastServices = inject(ToastService);
readonly settlementTypeItems = [
{
label: 'نقدی',
value: 'CASH',
},
{
label: 'نسیه',
value: 'CREDIT',
},
{
label: 'نقدی / نسیه',
value: 'MIXED',
},
];
readonly selectedSettlementType = signal<'CASH' | 'CREDIT' | 'MIXED'>('CASH');
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
readonly remainedAmount = signal(this.totalAmount());
@@ -52,7 +74,8 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
cashMax = signal(this.remainedAmount());
terminalsMax = signal([this.remainedAmount()]);
submitOrderLoading = computed(() => this.store.submitOrderLoading());
readonly submitOrderLoading = computed(() => this.store.submitOrderLoading());
readonly isGoldGuild = computed(() => this.posInfo.entity()?.guild.code.toLowerCase() === 'gold');
payByTerminalSteps = signal(
Array.from({ length: 10 }, (_, i) => ({
@@ -260,12 +283,16 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}
override submitForm() {
if (this.remainedAmount() > 0) {
return this.toastServices.warn({
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
});
if (this.selectedSettlementType() === 'CASH') {
if (this.remainedAmount() > 0) {
return this.toastServices.warn({
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
});
}
}
this.store.setSettlementType(this.selectedSettlementType());
const rawPayment = this.form.getRawValue();
const terminalPayments = this.payByTerminalSteps()
@@ -292,9 +319,9 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
const terminalPaymentIsDone = this.checkTerminalPayments();
if (terminalPaymentIsDone) {
this.toastService.info({
text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
});
// this.toastService.info({
// text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
// });
this.store
.submitOrder()
.pipe(
@@ -35,7 +35,7 @@
}
</div>
@if (showPayloadForm() && selectedGoodToAdd(); as selectedGood) {
@if (selectedGoodToAdd(); as selectedGood) {
<pos-payload-form-dialog
[(visible)]="showPayloadForm"
[good]="selectedGood"
+1
View File
@@ -8,6 +8,7 @@ import { IPosOrderItem } from './types';
export interface IPosOrderRequest {
total_amount: number;
invoice_date: string;
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
payments: IPayment;
items: IPosOrderItem[];
notes?: string;
@@ -29,6 +29,7 @@ interface IPosLandingState {
};
invoiceDate: string;
orderNote?: string;
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
payments: IPayment;
submitOrderLoading: boolean;
}
@@ -42,6 +43,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
inOrderGoods: [],
viewType: 'grid',
searchQuery: '',
settlement_type: 'CASH',
customerDetails: {
type: CustomerType.UNKNOWN,
},
@@ -81,7 +83,7 @@ export class PosLandingStore {
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
});
readonly filteredGoods = computed(() =>
this.activatedCategoryGoods()?.filter((good) => good.name.includes(this.state$().searchQuery)),
this.activatedCategoryGoods()?.filter((good) => good.name.includes(this.state$().searchQuery))
);
readonly customer = computed(() => this.state$().customerDetails);
@@ -251,6 +253,10 @@ export class PosLandingStore {
this.setState({ payments });
}
setSettlementType(settlement_type: 'CASH' | 'CREDIT' | 'MIXED') {
this.setState({ settlement_type });
}
submitOrder() {
this.setState({ submitOrderLoading: true });
const orderPayload: IPosOrderRequest = {
@@ -264,12 +270,13 @@ export class PosLandingStore {
})),
invoice_date: new Date().toISOString(),
payments: this.payments(),
settlement_type: this.state$().settlement_type,
total_amount: this.orderPricingInfo().totalAmount,
};
this.toastServices.info({
text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
});
// this.toastServices.info({
// text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
// });
let res: Maybe<IPosOrderResponse> = null;
@@ -297,7 +304,7 @@ export class PosLandingStore {
}
return _res;
}),
})
);
}
}
@@ -33,11 +33,9 @@
</div>
</button>
}
@if (showInvoiceBottomSheet()) {
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [modal]="true" [closable]="true" header="فاکتور">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet>
}
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات فاکتور">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet>
@if (isVisiblePaymentForm()) {
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
@@ -63,7 +63,7 @@ export abstract class AbstractForm<
finalize(() => {
this.submitLoading.set(false);
// this.form.enable();
}),
})
)
.subscribe((res) => {
this.onSuccess(res);
@@ -132,12 +132,16 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
this.syncZIndex();
}
if (changes['visible']) {
console.log('changed', this.visible);
this.toggleBodyScrollLock(this.visible);
}
}
ngOnDestroy() {
this.toggleBodyScrollLock(false);
console.log('destroyed');
this.removeDrawer();
}
@@ -167,6 +171,8 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
}
private toggleBodyScrollLock(locked: boolean) {
console.log('locked', locked);
const body = this.document.body;
if (!body) return;
@@ -182,8 +188,8 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
return;
}
this.renderer.setStyle(body, 'overflow', this.previousBodyOverflow);
this.renderer.setStyle(body, 'touch-action', this.previousBodyTouchAction);
this.renderer.removeStyle(body, 'overflow');
this.renderer.removeStyle(body, 'touch-action');
this.previousBodyOverflow = '';
this.previousBodyTouchAction = '';
}
@@ -4,10 +4,19 @@ import { InputComponent } from '../input/input.component';
@Component({
selector: 'field-quantity',
template: `<app-input label="مقدار" [control]="control" [name]="name" type="number" />`,
template: `<app-input
label="مقدار"
[control]="control"
[name]="name"
type="number"
[min]="min"
[max]="max"
/>`,
imports: [ReactiveFormsModule, InputComponent],
})
export class QuantityComponent {
@Input({ required: true }) control = new FormControl<string>('');
@Input() min = 0;
@Input() max = undefined;
@Input() name = 'quantity';
}