feat: enhance form components with validation and dynamic properties; update payment handling and UI interactions
This commit is contained in:
@@ -1,98 +1,188 @@
|
|||||||
# AGENT.md
|
# AGENT.md
|
||||||
|
|
||||||
## Purpose
|
## Scope
|
||||||
|
|
||||||
- This file defines repository-specific instructions for coding agents.
|
Applies to the full repository unless overridden by a deeper `AGENT.md`.
|
||||||
- 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.
|
Stack:
|
||||||
|
- Angular 20
|
||||||
|
- Standalone components
|
||||||
|
- pnpm
|
||||||
|
- Docker
|
||||||
|
- RTK
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# ⚡ EXECUTION RULES (STRICT)
|
# EXECUTION RULES
|
||||||
|
|
||||||
Minimize reasoning verbosity.
|
- Keep responses short.
|
||||||
|
- Do not narrate thoughts.
|
||||||
## Response Style
|
- Do not explain obvious steps.
|
||||||
|
- Do not create plans for simple tasks.
|
||||||
Do not:
|
- Prefer implementation over exploration.
|
||||||
- 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
|
|
||||||
|
|
||||||
Avoid:
|
Avoid:
|
||||||
- repeated plan updates
|
- “I think…”
|
||||||
- verbose planning
|
- “Let me check…”
|
||||||
- unnecessary decomposition
|
- “I should inspect…”
|
||||||
|
- “I’m going to…”
|
||||||
---
|
|
||||||
|
|
||||||
# ✂️ 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
|
|
||||||
|
|
||||||
Prefer:
|
Prefer:
|
||||||
|
- “Updated environment files.”
|
||||||
Updated environment files for development, staging, production, and tis.
|
- “Applied tenant config fix.”
|
||||||
|
|
||||||
Avoid:
|
|
||||||
|
|
||||||
I found a mismatch and then investigated several files before determining that...
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 📦 FINAL RESPONSE RULES
|
# RTK RULES (MANDATORY)
|
||||||
|
|
||||||
Final responses must be under 10 lines unless:
|
Always prefer RTK commands.
|
||||||
|
|
||||||
- user explicitly asks for explanation
|
Use:
|
||||||
- architectural decisions changed
|
|
||||||
|
- `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
|
- validation failed
|
||||||
- multiple systems were affected
|
- architecture changed
|
||||||
|
- user requested explanation
|
||||||
|
|
||||||
Prefer:
|
Prefer:
|
||||||
|
|
||||||
@@ -101,729 +191,4 @@ Updated:
|
|||||||
- file2
|
- file2
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
- pnpm tsc passed
|
- 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.
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class NativeBridgeService {
|
|||||||
window.NativeBridge.pay(request.amount, request.id || '');
|
window.NativeBridge.pay(request.amount, request.id || '');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} 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 };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
// const fn = window.NativeBridge.pay(123, 'test');
|
// const fn = window.NativeBridge.pay(123, 'test');
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export function greaterThanValidator(minValue: number): ValidatorFn {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('v', v);
|
||||||
|
|
||||||
const numericValue = typeof v === 'number' ? v : Number(v);
|
const numericValue = typeof v === 'number' ? v : Number(v);
|
||||||
if (Number.isNaN(numericValue)) {
|
if (Number.isNaN(numericValue)) {
|
||||||
return { greaterThan: 'مقدار باید عددی باشد' };
|
return { greaterThan: 'مقدار باید عددی باشد' };
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
[minAmount]="0"
|
[minAmount]="0"
|
||||||
[maxAmount]="unitWithQuantity()"
|
[maxAmount]="unitWithQuantity()"
|
||||||
name="wages"
|
name="wages"
|
||||||
label="اجرت"
|
label="اجرت" />
|
||||||
/>
|
|
||||||
|
|
||||||
{{ initialValues?.payload?.profit }}
|
{{ initialValues?.payload?.profit }}
|
||||||
<app-amount-percentage-input
|
<app-amount-percentage-input
|
||||||
@@ -22,8 +21,7 @@
|
|||||||
[minAmount]="0"
|
[minAmount]="0"
|
||||||
[maxAmount]="unitWithQuantity()"
|
[maxAmount]="unitWithQuantity()"
|
||||||
name="commission"
|
name="commission"
|
||||||
label="حقالعمل"
|
label="حقالعمل" />
|
||||||
/>
|
|
||||||
<app-amount-percentage-input
|
<app-amount-percentage-input
|
||||||
[percentageControl]="form.controls.payload.controls.profit_percentage"
|
[percentageControl]="form.controls.payload.controls.profit_percentage"
|
||||||
[amountControl]="form.controls.payload.controls.profit"
|
[amountControl]="form.controls.payload.controls.profit"
|
||||||
@@ -31,8 +29,7 @@
|
|||||||
[minAmount]="0"
|
[minAmount]="0"
|
||||||
[maxAmount]="totalAmountBeforeProfit()"
|
[maxAmount]="totalAmountBeforeProfit()"
|
||||||
name="profit"
|
name="profit"
|
||||||
label="سود"
|
label="سود" />
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<app-amount-percentage-input
|
<app-amount-percentage-input
|
||||||
@@ -42,8 +39,7 @@
|
|||||||
[minAmount]="0"
|
[minAmount]="0"
|
||||||
[maxAmount]="baseAmountForDiscountCalculation()"
|
[maxAmount]="baseAmountForDiscountCalculation()"
|
||||||
name="discount"
|
name="discount"
|
||||||
label="تخفیف"
|
label="تخفیف">
|
||||||
>
|
|
||||||
<ng-template #labelSuffix>
|
<ng-template #labelSuffix>
|
||||||
<p-selectButton
|
<p-selectButton
|
||||||
[options]="discountTypeItems"
|
[options]="discountTypeItems"
|
||||||
@@ -51,19 +47,17 @@
|
|||||||
[allowEmpty]="false"
|
[allowEmpty]="false"
|
||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="value"
|
optionValue="value"
|
||||||
(onChange)="changeDiscountCalculation($event)"
|
(onChange)="changeDiscountCalculation($event)" />
|
||||||
/>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-amount-percentage-input>
|
</app-amount-percentage-input>
|
||||||
|
|
||||||
<hr />
|
<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
|
<pos-form-dialog-amount-card-template
|
||||||
[totalAmount]="totalAmount()"
|
[totalAmount]="totalAmount()"
|
||||||
[baseTotalAmount]="baseTotalAmount()"
|
[baseTotalAmount]="baseTotalAmount()"
|
||||||
[discountAmount]="form.controls.discount.value || 0"
|
[discountAmount]="form.controls.discount.value || 0"
|
||||||
[taxAmount]="taxAmount()"
|
[taxAmount]="taxAmount()" />
|
||||||
/>
|
<button pButton class="w-full sm:w-auto" (click)="submit()">{{ preparedCTAText() }}</button>
|
||||||
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
|
|||||||
const form = this.fb.group({
|
const form = this.fb.group({
|
||||||
unit_price: [
|
unit_price: [
|
||||||
this.initialValues?.unit_price || this.goldDefaultUnitPrice() || 0,
|
this.initialValues?.unit_price || this.goldDefaultUnitPrice() || 0,
|
||||||
[Validators.required],
|
[Validators.required, greaterThanValidator(0)],
|
||||||
],
|
],
|
||||||
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
|
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
|
||||||
discount: [this.initialValues?.discount || 0],
|
discount: [this.initialValues?.discount || 0],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ISummary from '@/core/models/summary';
|
import ISummary from '@/core/models/summary';
|
||||||
|
import { greaterThanValidator } from '@/core/validators';
|
||||||
import { AbstractForm } from '@/shared/abstractClasses';
|
import { AbstractForm } from '@/shared/abstractClasses';
|
||||||
import { InputComponent } from '@/shared/components';
|
import { InputComponent } from '@/shared/components';
|
||||||
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
|
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
|
||||||
@@ -32,8 +33,11 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
|
|||||||
|
|
||||||
private readonly initialForm = () => {
|
private readonly initialForm = () => {
|
||||||
const form = this.fb.group({
|
const form = this.fb.group({
|
||||||
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
|
unit_price: [
|
||||||
quantity: [this.initialValues?.quantity || 0, [Validators.required]],
|
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_amount: [this.initialValues?.discount || 0, [Validators.min(0)]],
|
||||||
discount_percentage: [0, [Validators.min(0), Validators.max(100)]],
|
discount_percentage: [0, [Validators.min(0), Validators.max(100)]],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
[modal]="true"
|
[modal]="true"
|
||||||
[style]="{ 'max-height': '90svh', height: 'auto' }"
|
[style]="{ 'max-height': '90svh', height: 'auto' }"
|
||||||
[closable]="true"
|
[closable]="true"
|
||||||
(onHide)="close()"
|
(onHide)="close()">
|
||||||
>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<form [formGroup]="form" (submit)="submit()">
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
|
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
|
||||||
@@ -14,6 +13,15 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="form-group">
|
<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">
|
<uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps">
|
||||||
<p-select
|
<p-select
|
||||||
[options]="payByTerminalSteps()"
|
[options]="payByTerminalSteps()"
|
||||||
@@ -23,8 +31,7 @@
|
|||||||
optionValue="value"
|
optionValue="value"
|
||||||
[disabled]="!remainedAmount()"
|
[disabled]="!remainedAmount()"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
(onChange)="changePayByTerminalSteps($event.value)"
|
(onChange)="changePayByTerminalSteps($event.value)" />
|
||||||
/>
|
|
||||||
</uikit-field>
|
</uikit-field>
|
||||||
@for (terminalControl of terminalControls; track $index) {
|
@for (terminalControl of terminalControls; track $index) {
|
||||||
<app-input
|
<app-input
|
||||||
@@ -33,16 +40,14 @@
|
|||||||
[label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
|
[label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
|
||||||
type="price"
|
type="price"
|
||||||
[max]="terminalsMax()[$index]"
|
[max]="terminalsMax()[$index]"
|
||||||
[disabled]="payByTerminalSteps()[$index].payed"
|
[disabled]="payByTerminalSteps()[$index].payed">
|
||||||
>
|
|
||||||
<ng-template #suffixTemp>
|
<ng-template #suffixTemp>
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
size="small"
|
size="small"
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
|
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
|
||||||
(click)="fillTerminalRemained($index)"
|
(click)="fillTerminalRemained($index)">
|
||||||
>
|
|
||||||
باقیمانده مبلغ
|
باقیمانده مبلغ
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -55,14 +60,14 @@
|
|||||||
name="cash"
|
name="cash"
|
||||||
label="نقدی/ کارتخوان دیگر/ کارت به کارت"
|
label="نقدی/ کارتخوان دیگر/ کارت به کارت"
|
||||||
type="price"
|
type="price"
|
||||||
[max]="cashMax()"
|
[max]="cashMax()">
|
||||||
>
|
|
||||||
<ng-template #suffixTemp>
|
<ng-template #suffixTemp>
|
||||||
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
|
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
|
||||||
باقیمانده مبلغ
|
باقیمانده مبلغ
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-input>
|
</app-input>
|
||||||
|
@if (isGoldGuild()) {
|
||||||
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
|
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
|
||||||
<ng-template #suffixTemp>
|
<ng-template #suffixTemp>
|
||||||
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
|
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
|
||||||
@@ -70,12 +75,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-input>
|
</app-input>
|
||||||
|
}
|
||||||
|
|
||||||
<app-form-footer-actions
|
<app-form-footer-actions
|
||||||
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
submitLabel="ثبت اطلاعات پرداخت و ادامه"
|
||||||
[loading]="submitOrderLoading()"
|
[loading]="submitOrderLoading()"
|
||||||
(onCancel)="close()"
|
(onCancel)="close()" />
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</shared-light-bottomsheet>
|
</shared-light-bottomsheet>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { NativeBridgeService } from '@/core/services';
|
import { NativeBridgeService } from '@/core/services';
|
||||||
import { ToastService } from '@/core/services/toast.service';
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { PosInfoStore } from '@/domains/pos/store';
|
||||||
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
import { AbstractFormDialog } from '@/shared/abstractClasses';
|
||||||
import { InputComponent, KeyValueComponent } from '@/shared/components';
|
import { InputComponent, KeyValueComponent } from '@/shared/components';
|
||||||
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
|
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
|
||||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.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 { Component, computed, inject, signal } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
import { Select } from 'primeng/select';
|
import { Select } from 'primeng/select';
|
||||||
|
import { SelectButton } from 'primeng/selectbutton';
|
||||||
import { catchError, throwError } from 'rxjs';
|
import { catchError, throwError } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
IPayment,
|
IPayment,
|
||||||
@@ -36,15 +38,35 @@ import { PosLandingStore } from '../../store/main.store';
|
|||||||
SharedLightBottomsheetComponent,
|
SharedLightBottomsheetComponent,
|
||||||
Select,
|
Select,
|
||||||
UikitFieldComponent,
|
UikitFieldComponent,
|
||||||
|
SelectButton,
|
||||||
|
UikitLabelComponent,
|
||||||
],
|
],
|
||||||
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
|
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
|
||||||
})
|
})
|
||||||
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
|
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
|
||||||
private readonly store = inject(PosLandingStore);
|
private readonly store = inject(PosLandingStore);
|
||||||
|
private readonly posInfo = inject(PosInfoStore);
|
||||||
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
|
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
|
||||||
private readonly nativeBridgeService = inject(NativeBridgeService);
|
private readonly nativeBridgeService = inject(NativeBridgeService);
|
||||||
private readonly toastServices = inject(ToastService);
|
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 totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
|
||||||
|
|
||||||
readonly remainedAmount = signal(this.totalAmount());
|
readonly remainedAmount = signal(this.totalAmount());
|
||||||
@@ -52,7 +74,8 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
|||||||
cashMax = signal(this.remainedAmount());
|
cashMax = signal(this.remainedAmount());
|
||||||
terminalsMax = 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(
|
payByTerminalSteps = signal(
|
||||||
Array.from({ length: 10 }, (_, i) => ({
|
Array.from({ length: 10 }, (_, i) => ({
|
||||||
@@ -260,11 +283,15 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override submitForm() {
|
override submitForm() {
|
||||||
|
if (this.selectedSettlementType() === 'CASH') {
|
||||||
if (this.remainedAmount() > 0) {
|
if (this.remainedAmount() > 0) {
|
||||||
return this.toastServices.warn({
|
return this.toastServices.warn({
|
||||||
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
|
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.setSettlementType(this.selectedSettlementType());
|
||||||
|
|
||||||
const rawPayment = this.form.getRawValue();
|
const rawPayment = this.form.getRawValue();
|
||||||
|
|
||||||
@@ -292,9 +319,9 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
|
|||||||
const terminalPaymentIsDone = this.checkTerminalPayments();
|
const terminalPaymentIsDone = this.checkTerminalPayments();
|
||||||
|
|
||||||
if (terminalPaymentIsDone) {
|
if (terminalPaymentIsDone) {
|
||||||
this.toastService.info({
|
// this.toastService.info({
|
||||||
text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
|
// text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
|
||||||
});
|
// });
|
||||||
this.store
|
this.store
|
||||||
.submitOrder()
|
.submitOrder()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (showPayloadForm() && selectedGoodToAdd(); as selectedGood) {
|
@if (selectedGoodToAdd(); as selectedGood) {
|
||||||
<pos-payload-form-dialog
|
<pos-payload-form-dialog
|
||||||
[(visible)]="showPayloadForm"
|
[(visible)]="showPayloadForm"
|
||||||
[good]="selectedGood"
|
[good]="selectedGood"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { IPosOrderItem } from './types';
|
|||||||
export interface IPosOrderRequest {
|
export interface IPosOrderRequest {
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
|
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
|
||||||
payments: IPayment;
|
payments: IPayment;
|
||||||
items: IPosOrderItem[];
|
items: IPosOrderItem[];
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface IPosLandingState {
|
|||||||
};
|
};
|
||||||
invoiceDate: string;
|
invoiceDate: string;
|
||||||
orderNote?: string;
|
orderNote?: string;
|
||||||
|
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
|
||||||
payments: IPayment;
|
payments: IPayment;
|
||||||
submitOrderLoading: boolean;
|
submitOrderLoading: boolean;
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
|
|||||||
inOrderGoods: [],
|
inOrderGoods: [],
|
||||||
viewType: 'grid',
|
viewType: 'grid',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
settlement_type: 'CASH',
|
||||||
customerDetails: {
|
customerDetails: {
|
||||||
type: CustomerType.UNKNOWN,
|
type: CustomerType.UNKNOWN,
|
||||||
},
|
},
|
||||||
@@ -81,7 +83,7 @@ export class PosLandingStore {
|
|||||||
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
|
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
|
||||||
});
|
});
|
||||||
readonly filteredGoods = computed(() =>
|
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);
|
readonly customer = computed(() => this.state$().customerDetails);
|
||||||
@@ -251,6 +253,10 @@ export class PosLandingStore {
|
|||||||
this.setState({ payments });
|
this.setState({ payments });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSettlementType(settlement_type: 'CASH' | 'CREDIT' | 'MIXED') {
|
||||||
|
this.setState({ settlement_type });
|
||||||
|
}
|
||||||
|
|
||||||
submitOrder() {
|
submitOrder() {
|
||||||
this.setState({ submitOrderLoading: true });
|
this.setState({ submitOrderLoading: true });
|
||||||
const orderPayload: IPosOrderRequest = {
|
const orderPayload: IPosOrderRequest = {
|
||||||
@@ -264,12 +270,13 @@ export class PosLandingStore {
|
|||||||
})),
|
})),
|
||||||
invoice_date: new Date().toISOString(),
|
invoice_date: new Date().toISOString(),
|
||||||
payments: this.payments(),
|
payments: this.payments(),
|
||||||
|
settlement_type: this.state$().settlement_type,
|
||||||
total_amount: this.orderPricingInfo().totalAmount,
|
total_amount: this.orderPricingInfo().totalAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.toastServices.info({
|
// this.toastServices.info({
|
||||||
text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
|
// text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
|
||||||
});
|
// });
|
||||||
|
|
||||||
let res: Maybe<IPosOrderResponse> = null;
|
let res: Maybe<IPosOrderResponse> = null;
|
||||||
|
|
||||||
@@ -297,7 +304,7 @@ export class PosLandingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return _res;
|
return _res;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (showInvoiceBottomSheet()) {
|
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات فاکتور">
|
||||||
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [modal]="true" [closable]="true" header="فاکتور">
|
|
||||||
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
|
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
|
||||||
</shared-light-bottomsheet>
|
</shared-light-bottomsheet>
|
||||||
}
|
|
||||||
|
|
||||||
@if (isVisiblePaymentForm()) {
|
@if (isVisiblePaymentForm()) {
|
||||||
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
|
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export abstract class AbstractForm<
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.submitLoading.set(false);
|
this.submitLoading.set(false);
|
||||||
// this.form.enable();
|
// this.form.enable();
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.subscribe((res) => {
|
.subscribe((res) => {
|
||||||
this.onSuccess(res);
|
this.onSuccess(res);
|
||||||
|
|||||||
@@ -132,12 +132,16 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
|
|||||||
this.syncZIndex();
|
this.syncZIndex();
|
||||||
}
|
}
|
||||||
if (changes['visible']) {
|
if (changes['visible']) {
|
||||||
|
console.log('changed', this.visible);
|
||||||
|
|
||||||
this.toggleBodyScrollLock(this.visible);
|
this.toggleBodyScrollLock(this.visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.toggleBodyScrollLock(false);
|
this.toggleBodyScrollLock(false);
|
||||||
|
console.log('destroyed');
|
||||||
|
|
||||||
this.removeDrawer();
|
this.removeDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +171,8 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toggleBodyScrollLock(locked: boolean) {
|
private toggleBodyScrollLock(locked: boolean) {
|
||||||
|
console.log('locked', locked);
|
||||||
|
|
||||||
const body = this.document.body;
|
const body = this.document.body;
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
@@ -182,8 +188,8 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderer.setStyle(body, 'overflow', this.previousBodyOverflow);
|
this.renderer.removeStyle(body, 'overflow');
|
||||||
this.renderer.setStyle(body, 'touch-action', this.previousBodyTouchAction);
|
this.renderer.removeStyle(body, 'touch-action');
|
||||||
this.previousBodyOverflow = '';
|
this.previousBodyOverflow = '';
|
||||||
this.previousBodyTouchAction = '';
|
this.previousBodyTouchAction = '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ import { InputComponent } from '../input/input.component';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'field-quantity',
|
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],
|
imports: [ReactiveFormsModule, InputComponent],
|
||||||
})
|
})
|
||||||
export class QuantityComponent {
|
export class QuantityComponent {
|
||||||
@Input({ required: true }) control = new FormControl<string>('');
|
@Input({ required: true }) control = new FormControl<string>('');
|
||||||
|
@Input() min = 0;
|
||||||
|
@Input() max = undefined;
|
||||||
@Input() name = 'quantity';
|
@Input() name = 'quantity';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user