feat: add public sale invoices module with routing and service integration
- Introduced PUBLIC_SALE_INVOICES_ROUTES for handling sale invoice routes. - Created PublicSaleInvoicesService to fetch single invoice data from the API. - Implemented PublicSaleInvoiceStore for managing invoice state. - Added PublicSaleInvoiceComponent to display invoice details. - Updated app routes to include public sale invoices. - Removed pre-invoice dialog component and its associated files. - Enhanced order submitted dialog with QR code for invoice sharing. - Updated sale invoice single view to reflect new data structure. - Refactored page data list component for improved layout and functionality.
This commit is contained in:
@@ -4,15 +4,251 @@
|
|||||||
|
|
||||||
- This file defines repository-specific instructions for coding agents.
|
- This file defines repository-specific instructions for coding agents.
|
||||||
- Scope is the full repo unless a deeper `AGENT.md` overrides it.
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ RTK-FIRST RULES (STRICT)
|
||||||
|
|
||||||
|
Always prefer RTK commands for repository inspection, navigation, and code understanding.
|
||||||
|
|
||||||
|
## Command Rewrite Rules
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📂 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:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
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
|
## Stack Context
|
||||||
|
|
||||||
- Frontend: Angular 20 standalone app.
|
- Frontend: Angular 20 standalone app.
|
||||||
- Package manager: `pnpm`.
|
- Package manager: `pnpm`.
|
||||||
- Deployment: Docker / Docker Compose with tenant-specific services.
|
- Deployment: Docker / Docker Compose with tenant-specific services.
|
||||||
- Current service mapping expectation:
|
- AI-assisted repository navigation: RTK.
|
||||||
- `app_default` on host port `8090`
|
|
||||||
- `app_tis` on host port `8091`
|
### Current service mapping expectation
|
||||||
|
|
||||||
|
- `app_default` on host port `8090`
|
||||||
|
- `app_tis` on host port `8091`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tenant Build Rules
|
## Tenant Build Rules
|
||||||
|
|
||||||
@@ -23,6 +259,8 @@
|
|||||||
- Do not assume `default` Angular configuration is usable unless verified (it may reference missing replacements).
|
- 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.
|
- Do not use Angular `fileReplacements` for static assets (`.png`, `.jpg`, etc.); use tenant public assets or prebuild copy scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tenant PWA Rules
|
## Tenant PWA Rules
|
||||||
|
|
||||||
- Keep tenant manifest files under tenant public assets (e.g. `public-tis/favicon/site.webmanifest`).
|
- Keep tenant manifest files under tenant public assets (e.g. `public-tis/favicon/site.webmanifest`).
|
||||||
@@ -31,46 +269,76 @@
|
|||||||
- subpath deploy example: `/tis/`
|
- subpath deploy example: `/tis/`
|
||||||
- Keep `<link rel="manifest">` path and branding config `manifestPath` aligned.
|
- Keep `<link rel="manifest">` path and branding config `manifestPath` aligned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Input Component Rules
|
## Input Component Rules
|
||||||
|
|
||||||
- File: `src/app/shared/components/input/input.component.ts`
|
File:
|
||||||
- For `type === 'number'` or `type === 'price'`:
|
- `src/app/shared/components/input/input.component.ts`
|
||||||
- Normalize Persian/Arabic digits to English digits.
|
|
||||||
- Allow only digits and `.`.
|
For:
|
||||||
- Support `fixed` precision formatting when provided.
|
- `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.
|
- Keep behavior for identifier fields (`mobile`, `phone`, `postalCode`, `nationalId`) string-safe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Change Policy
|
## Change Policy
|
||||||
|
|
||||||
- Keep changes minimal and scoped to user request.
|
- Keep changes minimal and scoped to user request.
|
||||||
- Prefer root-cause fixes over temporary workarounds.
|
- Prefer root-cause fixes over temporary workarounds.
|
||||||
- Avoid unrelated refactors.
|
- Avoid unrelated refactors.
|
||||||
- Reuse existing patterns and naming conventions.
|
- Reuse existing patterns and naming conventions.
|
||||||
|
- Use RTK to understand existing patterns before introducing new ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Do / Don't
|
## Do / Don't
|
||||||
|
|
||||||
- Do follow existing field wrapper style in `src/app/shared/components/fields/*.component.ts`.
|
### Do
|
||||||
- Do reuse `app-input` and set only required props (`type`, `label`, `name`, constraints).
|
|
||||||
- Do register every new field in:
|
- 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/components/fields/index.ts`
|
||||||
- `src/app/shared/constants/fields/index.ts`
|
- `src/app/shared/constants/fields/index.ts`
|
||||||
- Do keep control keys consistent across form group, field component `name`, and `fieldControl` key.
|
- 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 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 use invalid Angular file replacements for directories or empty paths.
|
||||||
- Don't change tenant output directories without updating Docker `DIST_DIR`.
|
- 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
|
## How To Create Form Fields
|
||||||
|
|
||||||
- Create a wrapper component in `src/app/shared/components/fields`.
|
- Create a wrapper component in:
|
||||||
|
- `src/app/shared/components/fields`
|
||||||
- Use the same pattern as existing files like:
|
- Use the same pattern as existing files like:
|
||||||
- `src/app/shared/components/fields/name.component.ts`
|
- `src/app/shared/components/fields/name.component.ts`
|
||||||
- `src/app/shared/components/fields/unit_price.component.ts`
|
- `src/app/shared/components/fields/unit_price.component.ts`
|
||||||
- Minimal wrapper shape:
|
|
||||||
- `selector`: `field-<field-name>`
|
|
||||||
- template: `<app-input ... />`
|
|
||||||
- inputs: `control` (required), optional `name`, optional `label`
|
|
||||||
|
|
||||||
Example pattern:
|
Minimal wrapper shape:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@Component({
|
@Component({
|
||||||
@@ -85,15 +353,19 @@ export class ExampleComponent {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Register New Fields
|
## Register New Fields
|
||||||
|
|
||||||
- Export the component from:
|
Export the component from:
|
||||||
- `src/app/shared/components/fields/index.ts`
|
- `src/app/shared/components/fields/index.ts`
|
||||||
- Add its form control factory in:
|
|
||||||
- `src/app/shared/constants/fields/index.ts`
|
Add its form control factory in:
|
||||||
- `fieldControl` entry shape:
|
- `src/app/shared/constants/fields/index.ts`
|
||||||
- key must match form control name
|
|
||||||
- return tuple: `[defaultValue, validators]`
|
`fieldControl` entry shape:
|
||||||
|
- key must match form control name
|
||||||
|
- return tuple: `[defaultValue, validators]`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -104,55 +376,157 @@ example: (value = '', isRequired = true): ControlConfig => [
|
|||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Using Fields In Forms
|
## Using Fields In Forms
|
||||||
|
|
||||||
- In form group builders, use `fieldControl.<key>(initialValue, isRequired)` for consistency.
|
- In form group builders, use:
|
||||||
- In templates, render matching wrapper component and pass the matching control:
|
- `fieldControl.<key>(initialValue, isRequired)`
|
||||||
|
- In templates, render matching wrapper component and pass matching control:
|
||||||
- `<field-example [control]="form.controls.example" />`
|
- `<field-example [control]="form.controls.example" />`
|
||||||
- For numeric/price behavior, use `app-input` `type="number"` or `type="price"` and optional `[fixed]`.
|
- For numeric/price behavior:
|
||||||
|
- use `app-input`
|
||||||
|
- use `type="number"` or `type="price"`
|
||||||
|
- optional `[fixed]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## List Component Configs
|
## List Component Configs
|
||||||
|
|
||||||
- Centralized list metadata is stored in `src/app/shared/constants/list-configs/` — **never** in domain modules.
|
Centralized list metadata is stored in:
|
||||||
- Structure by data type, not domain (e.g. `good-list.const.ts`, `sku-list.const.ts`, `category-list.const.ts`).
|
- `src/app/shared/constants/list-configs/`
|
||||||
- Each config implements `IListConfig` (defined in `list-config.model.ts`):
|
|
||||||
- `pageTitle`: display title for the list page
|
Never store configs inside domain modules.
|
||||||
- `addNewCtaLabel`: call-to-action button label
|
|
||||||
- `emptyPlaceholderTitle` and `emptyPlaceholderDescription`: empty state messaging
|
### Structure
|
||||||
- `columns`: array of `IColumn[]` definitions
|
|
||||||
- Usage in list components:
|
Structure by data type, not domain:
|
||||||
```ts
|
- `good-list.const.ts`
|
||||||
@Input() header: IColumn[] = goodListConfig.columns;
|
- `sku-list.const.ts`
|
||||||
listConfig = goodListConfig;
|
- `category-list.const.ts`
|
||||||
```
|
|
||||||
- Any domain needing a list config imports from `@/shared/constants/list-configs` — no cross-domain dependencies.
|
### Requirements
|
||||||
- **Do not** define configs inside domains or duplicate them across modules.
|
|
||||||
|
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
|
## Breadcrumb Usage in Stores & Views
|
||||||
|
|
||||||
- Entity stores expose `breadcrumbItems` as a computed signal.
|
- Entity stores expose `breadcrumbItems` as a computed signal.
|
||||||
- Call `store.breadcrumbItems()` in view components and extend with current page:
|
- Call `store.breadcrumbItems()` in view components and extend with current page:
|
||||||
```ts
|
|
||||||
setBreadcrumb() {
|
```ts
|
||||||
|
setBreadcrumb() {
|
||||||
this.breadcrumbService.setItems([
|
this.breadcrumbService.setItems([
|
||||||
...this.store.breadcrumbItems(),
|
...this.store.breadcrumbItems(),
|
||||||
{ title: 'Current Page' },
|
{ title: 'Current Page' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Root page breadcrumbs are set in the store's `getData()` method once entity is loaded.
|
- Root page breadcrumbs are set in the store's `getData()` method once entity is loaded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
- For TypeScript-only changes, run:
|
### TypeScript-only changes
|
||||||
- `pnpm -s exec tsc -p tsconfig.app.json --noEmit`
|
|
||||||
- For Docker/build changes, verify with:
|
```bash
|
||||||
- `docker compose build app_default`
|
pnpm -s exec tsc -p tsconfig.app.json --noEmit
|
||||||
- `docker compose build app_tis`
|
```
|
||||||
- Start with targeted validation, then broader checks only if needed.
|
|
||||||
|
### 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
|
## Communication Expectations
|
||||||
|
|
||||||
- Report exactly which files changed and why.
|
- Report exactly which files changed and why.
|
||||||
- Call out any assumptions or discovered config mismatches.
|
- Call out assumptions and discovered config mismatches.
|
||||||
- If validation is blocked (permissions, missing dependencies), state it clearly and provide next command.
|
- 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.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@primeuix/themes": "^1.2.5",
|
"@primeuix/themes": "^1.2.5",
|
||||||
"@tailwindcss/postcss": "^4.2.3",
|
"@tailwindcss/postcss": "^4.2.3",
|
||||||
"@zoomit/dayjs-jalali-plugin": "^0.1.11",
|
"@zoomit/dayjs-jalali-plugin": "^0.1.11",
|
||||||
|
"angularx-qrcode": "20.0.0",
|
||||||
"chart.js": "4.4.2",
|
"chart.js": "4.4.2",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
|
|||||||
Generated
+311
-179
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -3,6 +3,7 @@ import { PARTNER_ROUTES } from '@/domains/partner/routes';
|
|||||||
import { POS_ROUTES } from '@/domains/pos/routes';
|
import { POS_ROUTES } from '@/domains/pos/routes';
|
||||||
import { PROVIDER_ROUTES } from '@/domains/provider/routes';
|
import { PROVIDER_ROUTES } from '@/domains/provider/routes';
|
||||||
import { SUPER_ADMIN_ROUTES } from '@/domains/superAdmin/routes';
|
import { SUPER_ADMIN_ROUTES } from '@/domains/superAdmin/routes';
|
||||||
|
import { PUBLIC_SALE_INVOICES_ROUTES } from '@/modules/saleInvoices/constants';
|
||||||
import { Notfound } from '@/pages/notfound/notfound.component';
|
import { Notfound } from '@/pages/notfound/notfound.component';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
@@ -23,6 +24,6 @@ export const appRoutes: Routes = [
|
|||||||
path: 'auth',
|
path: 'auth',
|
||||||
loadComponent: () => import('@/modules/auth/pages/auth.component').then((m) => m.AuthComponent),
|
loadComponent: () => import('@/modules/auth/pages/auth.component').then((m) => m.AuthComponent),
|
||||||
},
|
},
|
||||||
{ path: 'notfound', component: Notfound },
|
...PUBLIC_SALE_INVOICES_ROUTES,
|
||||||
{ path: '**', redirectTo: '/notfound' },
|
{ path: '**', component: Notfound },
|
||||||
];
|
];
|
||||||
|
|||||||
+7
-5
@@ -4,13 +4,15 @@
|
|||||||
[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="flex flex-col items-center justify-center gap-4 pt-10 pb-14 text-center">
|
||||||
<div class="text-center pt-10 pb-14 flex flex-col items-center justify-center gap-4">
|
<qrcode [qrdata]="publicInvoiceRoute()" [width]="220" [errorCorrectionLevel]="'M'"></qrcode>
|
||||||
<i class="" class="pi pi-check-circle text-6xl! text-green-700"></i>
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="" class="pi pi-check-circle text-xl! text-green-700"></i>
|
||||||
<p class="text-lg font-bold">فاکتور شما با موفقیت ایجاد شد.</p>
|
<p class="text-lg font-bold">فاکتور شما با موفقیت ایجاد شد.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 justify-center">
|
</div>
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
<button pButton type="button" outlined (click)="printInvoice()">چاپ</button>
|
<button pButton type="button" outlined (click)="printInvoice()">چاپ</button>
|
||||||
<button pButton type="button" outlined (click)="sendInvoice()">ارسال به سامانه مودیان</button>
|
<button pButton type="button" outlined (click)="sendInvoice()">ارسال به سامانه مودیان</button>
|
||||||
<button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button>
|
<button pButton type="button" outlined (click)="openOrderDetails()">جزئیات</button>
|
||||||
|
|||||||
+7
-2
@@ -6,6 +6,7 @@ import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/ligh
|
|||||||
import { formatJalali } from '@/utils';
|
import { formatJalali } from '@/utils';
|
||||||
import { Component, computed, inject, Input, signal } from '@angular/core';
|
import { Component, computed, inject, Input, signal } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { QRCodeComponent } from 'angularx-qrcode';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
import { finalize } from 'rxjs';
|
import { finalize } from 'rxjs';
|
||||||
import { posSaleInvoicesNamedRoutes } from '../../../saleInvoices/constants';
|
import { posSaleInvoicesNamedRoutes } from '../../../saleInvoices/constants';
|
||||||
@@ -15,7 +16,7 @@ import { IPosOrderResponse } from '../../models';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-order-submitted-dialog',
|
selector: 'pos-order-submitted-dialog',
|
||||||
templateUrl: 'order-submitted-dialog.component.html',
|
templateUrl: 'order-submitted-dialog.component.html',
|
||||||
imports: [SharedLightBottomsheetComponent, ButtonDirective],
|
imports: [SharedLightBottomsheetComponent, ButtonDirective, QRCodeComponent],
|
||||||
})
|
})
|
||||||
export class PosOrderSubmittedDialogComponent extends AbstractDialog {
|
export class PosOrderSubmittedDialogComponent extends AbstractDialog {
|
||||||
@Input({ required: true }) invoice!: IPosOrderResponse;
|
@Input({ required: true }) invoice!: IPosOrderResponse;
|
||||||
@@ -33,12 +34,16 @@ export class PosOrderSubmittedDialogComponent extends AbstractDialog {
|
|||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
readonly publicInvoiceRoute = computed(
|
||||||
|
() => `${window.location.origin}/sale-invoices/${this.invoice.id}`
|
||||||
|
);
|
||||||
|
|
||||||
sendingLoading = signal(false);
|
sendingLoading = signal(false);
|
||||||
sended = signal(false);
|
sended = signal(false);
|
||||||
|
|
||||||
openOrderDetails() {
|
openOrderDetails() {
|
||||||
this.router.navigateByUrl(
|
this.router.navigateByUrl(
|
||||||
posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.invoice.id),
|
posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.invoice.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<shared-light-bottomsheet
|
|
||||||
[(visible)]="visible"
|
|
||||||
header="پیش فاکتور"
|
|
||||||
[modal]="true"
|
|
||||||
[style]="{ 'max-height': '90svh', height: 'auto' }"
|
|
||||||
[closable]="true"
|
|
||||||
(onHide)="close()"
|
|
||||||
>
|
|
||||||
<form [formGroup]="form"></form>
|
|
||||||
<uikit-datepicker [control]="form.controls.invoiceDate" label="تاریخ فاکتور" />
|
|
||||||
</shared-light-bottomsheet>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { AbstractDialog } from '@/shared/abstractClasses/abstract-dialog';
|
|
||||||
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
|
|
||||||
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
|
||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
||||||
import { PosLandingStore } from '../store/main.store';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pos-pre-invoice-dialog',
|
|
||||||
templateUrl: './pre-invoice-dialog.component.html',
|
|
||||||
imports: [SharedLightBottomsheetComponent, UikitFlatpickrJalaliComponent, ReactiveFormsModule],
|
|
||||||
})
|
|
||||||
export class PosPreInvoiceDialogComponent extends AbstractDialog {
|
|
||||||
private readonly store = inject(PosLandingStore);
|
|
||||||
private readonly fb = inject(FormBuilder);
|
|
||||||
|
|
||||||
private initForm() {
|
|
||||||
const form = this.fb.group({
|
|
||||||
invoiceDate: [this.store.invoiceDate(), [Validators.required]],
|
|
||||||
});
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
form = this.initForm();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const baseUrl = () => `/api/v1/public-invoices`;
|
||||||
|
|
||||||
|
export const PUBLIC_SALE_INVOICES_API_ROUTES = {
|
||||||
|
single: (invoiceId: string) => `${baseUrl()}/${invoiceId}`,
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './apiRoutes/index';
|
||||||
|
export * from './routes/index';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { NamedRoutes } from '@/core';
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export type TPublicSaleInvoicesRouteNames = 'saleInvoice';
|
||||||
|
|
||||||
|
export const publicSaleInvoicesNamedRoutes: NamedRoutes<TPublicSaleInvoicesRouteNames> = {
|
||||||
|
saleInvoice: {
|
||||||
|
path: 'sale-invoices/:invoiceId',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/single.component').then((m) => m.PublicSaleInvoiceComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'جزئیات فاکتور',
|
||||||
|
pagePath: (invoiceId: string) => `sale-invoices/${invoiceId}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUBLIC_SALE_INVOICES_ROUTES: Routes = [publicSaleInvoicesNamedRoutes.saleInvoice];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './io';
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import ISummary from '@/core/models/summary';
|
||||||
|
import { IEnumTranslate } from '@/shared/models/enum_translate.type';
|
||||||
|
|
||||||
|
export interface IPublicSaleInvoicesRawResponse {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
invoice_date: string;
|
||||||
|
invoice_number: string;
|
||||||
|
total_amount: string;
|
||||||
|
pos: Pos;
|
||||||
|
consumer_account: ConsumerAccount;
|
||||||
|
payments: Payments[];
|
||||||
|
type: IEnumTranslate;
|
||||||
|
status: IEnumTranslate;
|
||||||
|
customer: Maybe<Customer>;
|
||||||
|
unknown_customer: Maybe<Record<string, unknown>>;
|
||||||
|
items: InvoiceItem[];
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicSaleInvoicesResponse extends IPublicSaleInvoicesRawResponse {}
|
||||||
|
|
||||||
|
interface ConsumerAccount {
|
||||||
|
role: string;
|
||||||
|
account: Account;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Account {
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Pos extends ISummary {
|
||||||
|
complex: Complex;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Complex extends ISummary {
|
||||||
|
business_activity: BA;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BA extends ISummary {
|
||||||
|
guild: ISummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Payments {
|
||||||
|
amount: string;
|
||||||
|
paid_at?: string;
|
||||||
|
payment_method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
type: string;
|
||||||
|
individual: Individual;
|
||||||
|
legal: Legal;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Individual {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
national_code: string;
|
||||||
|
mobile_number: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Legal {
|
||||||
|
company_name: string;
|
||||||
|
economic_code: string;
|
||||||
|
registration_number: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvoiceItem {
|
||||||
|
good_snapshot: GoodSnapshot;
|
||||||
|
measure_unit_text: string;
|
||||||
|
quantity: string;
|
||||||
|
total_amount: string;
|
||||||
|
unit_price: string;
|
||||||
|
discount: string;
|
||||||
|
payload: InvoiceItemPayload;
|
||||||
|
sku_code: string;
|
||||||
|
sku_vat: string;
|
||||||
|
notes: Maybe<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvoiceItemPayload {
|
||||||
|
karat: string;
|
||||||
|
wages: number;
|
||||||
|
profit: number;
|
||||||
|
commission: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoodSnapshot {
|
||||||
|
good: Good;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Good {
|
||||||
|
id: string;
|
||||||
|
sku: Sku;
|
||||||
|
name: string;
|
||||||
|
barcode: Maybe<string>;
|
||||||
|
category: ISummary;
|
||||||
|
image_url: string;
|
||||||
|
local_sku: Maybe<string>;
|
||||||
|
measure_unit: MeasureUnit;
|
||||||
|
pricing_model: string;
|
||||||
|
base_sale_price: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MeasureUnit extends ISummary {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sku extends ISummary {
|
||||||
|
VAT: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ISaleInvoiceFullResponse } from '@/domains/consumer/models';
|
||||||
|
import { ISaleInvoiceFullRawResponse } from '@/shared/components/invoices/sale-invoice-full-response.model';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PUBLIC_SALE_INVOICES_API_ROUTES } from '../constants';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class PublicSaleInvoicesService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private apiRoutes = PUBLIC_SALE_INVOICES_API_ROUTES;
|
||||||
|
|
||||||
|
getSingle(invoiceId: string): Observable<ISaleInvoiceFullResponse> {
|
||||||
|
return this.http.get<ISaleInvoiceFullRawResponse>(this.apiRoutes.single(invoiceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state';
|
||||||
|
import { ISaleInvoiceFullResponse } from '@/domains/consumer/models';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { catchError, finalize } from 'rxjs';
|
||||||
|
import { PublicSaleInvoicesService } from '../services/main.service';
|
||||||
|
|
||||||
|
interface PublicSaleInvoiceState extends EntityState<ISaleInvoiceFullResponse> {}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PublicSaleInvoiceStore extends EntityStore<
|
||||||
|
ISaleInvoiceFullResponse,
|
||||||
|
PublicSaleInvoiceState
|
||||||
|
> {
|
||||||
|
private readonly service = inject(PublicSaleInvoicesService);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
...defaultBaseStateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(invoiceId: string) {
|
||||||
|
this.patchState({ loading: true });
|
||||||
|
this.service
|
||||||
|
.getSingle(invoiceId)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.patchState({ loading: false });
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.setError(error);
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((entity) => {
|
||||||
|
console.log('entity', entity);
|
||||||
|
|
||||||
|
this.patchState({ entity });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override reset(): void {
|
||||||
|
this.setState({
|
||||||
|
...defaultBaseStateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './single.component';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="p-4">
|
||||||
|
<shared-sale-invoice-single-view [loading]="loading()" [invoice]="invoice()" />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component';
|
||||||
|
import pageParamsUtils from '@/utils/page-params.utils';
|
||||||
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { PublicSaleInvoiceStore } from '../store/main.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'public-saleInvoice',
|
||||||
|
templateUrl: './single.component.html',
|
||||||
|
imports: [SharedSaleInvoiceSingleViewComponent],
|
||||||
|
})
|
||||||
|
export class PublicSaleInvoiceComponent {
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly store = inject(PublicSaleInvoiceStore);
|
||||||
|
|
||||||
|
pageParams = computed(() => pageParamsUtils(this.route));
|
||||||
|
invoiceId = signal<string>(this.pageParams()['invoiceId']);
|
||||||
|
|
||||||
|
readonly invoice = computed(() => this.store.entity());
|
||||||
|
readonly loading = computed(() => this.store.loading());
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.store.getData(this.invoiceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,6 @@ import { Button } from 'primeng/button';
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(15, 23, 42, 0.22);
|
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 {
|
.light-bottomsheet-panel {
|
||||||
@@ -45,10 +43,7 @@ import { Button } from 'primeng/button';
|
|||||||
background: var(--surface-card, #fff);
|
background: var(--surface-card, #fff);
|
||||||
border-radius: 12px 12px 0 0;
|
border-radius: 12px 12px 0 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
will-change: transform;
|
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
animation: sheetSlideUp var(--sheet-transition, 130ms cubic-bezier(0.2, 0, 0, 1));
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,24 +82,6 @@ import { Button } from 'primeng/button';
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 1.25rem;
|
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: {
|
host: {
|
||||||
@@ -139,7 +116,7 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly elementRef: ElementRef<HTMLElement>,
|
private readonly elementRef: ElementRef<HTMLElement>,
|
||||||
private readonly renderer: Renderer2,
|
private readonly renderer: Renderer2,
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
@Inject(DOCUMENT) private readonly document: Document
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -214,7 +191,7 @@ export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnCha
|
|||||||
private syncZIndex() {
|
private syncZIndex() {
|
||||||
const host = this.elementRef.nativeElement;
|
const host = this.elementRef.nativeElement;
|
||||||
const siblingDrawers = Array.from(
|
const siblingDrawers = Array.from(
|
||||||
this.document.body.querySelectorAll('.p-drawer'),
|
this.document.body.querySelectorAll('.p-drawer')
|
||||||
) as HTMLElement[];
|
) as HTMLElement[];
|
||||||
const drawerZIndexes = siblingDrawers
|
const drawerZIndexes = siblingDrawers
|
||||||
.map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10))
|
.map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10))
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
icon="pi pi-print"
|
icon="pi pi-print"
|
||||||
outlined
|
outlined
|
||||||
size="small"
|
size="small"
|
||||||
(click)="printInvoice()"
|
(click)="printInvoice()"></button>
|
||||||
></button>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="listKeyValue">
|
<div class="listKeyValue">
|
||||||
@@ -29,14 +28,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p-divider align="center"> اطلاعات پرداخت </p-divider>
|
<p-divider align="center"> اطلاعات پرداخت </p-divider>
|
||||||
<div class="grid md:grid-cols-3 sm:grid-cols-2 sm:gap-4 gap-3 items-center">
|
<div class="grid items-center gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
||||||
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
|
<app-key-value label="مجموع قابل پرداخت" [value]="invoice.total_amount" type="price" />
|
||||||
@for (payment of invoice.payments; track $index) {
|
@for (payment of invoice.payments; track $index) {
|
||||||
<app-key-value
|
<app-key-value
|
||||||
[label]="`${payment.payment_method === 'SET_OFF' ? 'تهاتر' : payment.payment_method === 'TERMINAL' ? 'پوز' : 'نقدی/ کارتخوان دیگر/ کارت به کارت'}`"
|
[label]="`${payment.payment_method === 'SET_OFF' ? 'تهاتر' : payment.payment_method === 'TERMINAL' ? 'پوز' : 'نقدی/ کارتخوان دیگر/ کارت به کارت'}`"
|
||||||
[value]="payment.amount"
|
[value]="payment.amount"
|
||||||
type="price"
|
type="price" />
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,14 +43,16 @@
|
|||||||
<app-key-value label="کسب و کار" [value]="invoice.pos!.complex.business_activity.name" />
|
<app-key-value label="کسب و کار" [value]="invoice.pos!.complex.business_activity.name" />
|
||||||
<app-key-value label="مجموعه" [value]="invoice.pos!.complex.name" />
|
<app-key-value label="مجموعه" [value]="invoice.pos!.complex.name" />
|
||||||
<app-key-value label="پایانه فروش" [value]="invoice.pos!.name" />
|
<app-key-value label="پایانه فروش" [value]="invoice.pos!.name" />
|
||||||
|
@if (invoice.consumer_account?.account?.username) {
|
||||||
<app-key-value label="ایجاد کننده" [value]="invoice.consumer_account.account.username" />
|
<app-key-value label="ایجاد کننده" [value]="invoice.consumer_account.account.username" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (variant !== "customer") {
|
@if (variant !== 'customer') {
|
||||||
<p-divider align="center"> اطلاعات مشتری </p-divider>
|
<p-divider align="center"> اطلاعات مشتری </p-divider>
|
||||||
@if (invoice.customer) {
|
@if (invoice.customer) {
|
||||||
<div class="listKeyValue">
|
<div class="listKeyValue">
|
||||||
@if (invoice.customer.type === "INDIVIDUAL") {
|
@if (invoice.customer.type === 'INDIVIDUAL') {
|
||||||
<app-key-value label="نوع مشتری" value="حقیقی" />
|
<app-key-value label="نوع مشتری" value="حقیقی" />
|
||||||
<app-key-value label="نام" [value]="invoice.customer.individual?.first_name" />
|
<app-key-value label="نام" [value]="invoice.customer.individual?.first_name" />
|
||||||
<app-key-value label="نام خانوادگی" [value]="invoice.customer.individual?.last_name" />
|
<app-key-value label="نام خانوادگی" [value]="invoice.customer.individual?.last_name" />
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<app-key-value label="کد ثبتی" [value]="invoice.unknown_customer.national_code" />
|
<app-key-value label="کد ثبتی" [value]="invoice.unknown_customer.national_code" />
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="text-center text-text-color pt-3 pb-5">اطلاعات مشتری ثبت نشده است.</p>
|
<p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -88,16 +88,10 @@
|
|||||||
[items]="invoice.items"
|
[items]="invoice.items"
|
||||||
[loading]="loading"
|
[loading]="loading"
|
||||||
pageTitle=""
|
pageTitle=""
|
||||||
[showRefresh]="false"
|
[showRefresh]="false">
|
||||||
>
|
|
||||||
<ng-template #totalAmount let-item>
|
<ng-template #totalAmount let-item>
|
||||||
@if (!item.discount_amount) {
|
@if (!item.discount_amount) {
|
||||||
<span [appPriceMask]="item.total_amount"></span>
|
<span [appPriceMask]="item.total_amount"></span>
|
||||||
} @else {
|
|
||||||
<!-- <div class="flex flex-col items-end">
|
|
||||||
<span class="line-through text-muted-color text-xs" [appPriceMask]="item.total_amount"></span>
|
|
||||||
<span [appPriceMask]="item.total_amount - item.discount_amount"></span>
|
|
||||||
</div> -->
|
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-page-data-list>
|
</app-page-data-list>
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export class SharedSaleInvoiceSingleViewComponent {
|
|||||||
header: 'عنوان',
|
header: 'عنوان',
|
||||||
type: 'nested',
|
type: 'nested',
|
||||||
nestedOption: {
|
nestedOption: {
|
||||||
path: 'good.name',
|
path: 'good_snapshot.name',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<div class="h-full bg-surface-overlay rounded-lg shadow border border-surface-border p-0! overflow-hidden">
|
<div
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-surface-overlay h-full overflow-hidden rounded-lg p-0!': true,
|
||||||
|
'border-surface-border border shadow': hasCaption(),
|
||||||
|
}">
|
||||||
<ng-template #captionTemplate let-isMobileView="isMobileView">
|
<ng-template #captionTemplate let-isMobileView="isMobileView">
|
||||||
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
||||||
<ng-container [ngTemplateOutlet]="caption">
|
<ng-container [ngTemplateOutlet]="caption">
|
||||||
<div class="flex justify-between items-center gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<h5 class="font-bold">{{ pageTitle }}</h5>
|
<h5 class="font-bold">{{ pageTitle }}</h5>
|
||||||
@if (showAdd || filter || showRefresh || moreActions) {
|
@if (showAdd || filter || showRefresh || moreActions) {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -15,8 +19,7 @@
|
|||||||
badgeSeverity="info"
|
badgeSeverity="info"
|
||||||
size="small"
|
size="small"
|
||||||
[badge]="isFiltered ? '1' : undefined"
|
[badge]="isFiltered ? '1' : undefined"
|
||||||
(click)="openFilter()"
|
(click)="openFilter()"></p-button>
|
||||||
></p-button>
|
|
||||||
}
|
}
|
||||||
@if (showRefresh) {
|
@if (showRefresh) {
|
||||||
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
|
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
|
||||||
@@ -36,8 +39,7 @@
|
|||||||
label="{{ addNewCtaLabel }}"
|
label="{{ addNewCtaLabel }}"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
size="small"
|
size="small"
|
||||||
(click)="openAddForm()"
|
(click)="openAddForm()"></p-button>
|
||||||
></p-button>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,8 +54,7 @@
|
|||||||
[description]="emptyPlaceholderDescription"
|
[description]="emptyPlaceholderDescription"
|
||||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||||
[showCTA]="showAdd"
|
[showCTA]="showAdd"
|
||||||
(ctaClick)="openAddForm()"
|
(ctaClick)="openAddForm()"></uikit-empty-state>
|
||||||
></uikit-empty-state>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #paginatorTemplate>
|
<ng-template #paginatorTemplate>
|
||||||
<app-paginator
|
<app-paginator
|
||||||
@@ -61,8 +62,7 @@
|
|||||||
[totalRecords]="totalRecords"
|
[totalRecords]="totalRecords"
|
||||||
[perPage]="perPage || 10"
|
[perPage]="perPage || 10"
|
||||||
[loading]="loading"
|
[loading]="loading"
|
||||||
(onChange)="onPage($event)"
|
(onChange)="onPage($event)" />
|
||||||
/>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
@if (!isMobile) {
|
@if (!isMobile) {
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
[showDelete]="showDelete"
|
[showDelete]="showDelete"
|
||||||
[showDetails]="showDetails"
|
[showDetails]="showDetails"
|
||||||
[showIndex]="showIndex"
|
[showIndex]="showIndex"
|
||||||
[hasCaption]="!!(pageTitle || showAdd || filter || showRefresh || moreActions)"
|
[hasCaption]="hasCaption()"
|
||||||
[expandable]="expandable"
|
[expandable]="expandable"
|
||||||
[expandableItemKey]="expandableItemKey"
|
[expandableItemKey]="expandableItemKey"
|
||||||
[expandColumns]="expandColumns"
|
[expandColumns]="expandColumns"
|
||||||
@@ -89,8 +89,7 @@
|
|||||||
(onEdit)="edit($event)"
|
(onEdit)="edit($event)"
|
||||||
(onDelete)="remove($event)"
|
(onDelete)="remove($event)"
|
||||||
(onDetails)="details($event)"
|
(onDetails)="details($event)"
|
||||||
(onChangePage)="onPage($event)"
|
(onChangePage)="onPage($event)">
|
||||||
>
|
|
||||||
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
||||||
<ng-template #captionBox>
|
<ng-template #captionBox>
|
||||||
<ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: false }" />
|
<ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: false }" />
|
||||||
@@ -117,14 +116,13 @@
|
|||||||
[showEdit]="showEdit"
|
[showEdit]="showEdit"
|
||||||
[showDelete]="showDelete"
|
[showDelete]="showDelete"
|
||||||
[showDetails]="showDetails"
|
[showDetails]="showDetails"
|
||||||
[hasCaption]="!!(pageTitle || showAdd || filter || showRefresh || moreActions)"
|
[hasCaption]="hasCaption()"
|
||||||
[expandable]="expandable"
|
[expandable]="expandable"
|
||||||
[expandableItemKey]="expandableItemKey"
|
[expandableItemKey]="expandableItemKey"
|
||||||
[expandColumns]="expandColumns"
|
[expandColumns]="expandColumns"
|
||||||
(onEdit)="edit($event)"
|
(onEdit)="edit($event)"
|
||||||
(onDelete)="remove($event)"
|
(onDelete)="remove($event)"
|
||||||
(onDetails)="details($event)"
|
(onDetails)="details($event)">
|
||||||
>
|
|
||||||
<ng-template #captionBox>
|
<ng-template #captionBox>
|
||||||
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
|
||||||
<ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: true }" />
|
<ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: true }" />
|
||||||
@@ -145,8 +143,7 @@
|
|||||||
(onHide)="closeFilter()"
|
(onHide)="closeFilter()"
|
||||||
header="فیلتر"
|
header="فیلتر"
|
||||||
class="contnet"
|
class="contnet"
|
||||||
styleClass="!w-[80vw] md:!w-96 md:!max-w-96 !max-w-sm"
|
styleClass="!w-[80vw] md:!w-96 md:!max-w-96 !max-w-sm">
|
||||||
>
|
|
||||||
<hr class="mt-0!" />
|
<hr class="mt-0!" />
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<ng-container [ngTemplateOutlet]="filter" [ngTemplateOutletContext]="{ $implicit: columns }"> </ng-container>
|
<ng-container [ngTemplateOutlet]="filter" [ngTemplateOutletContext]="{ $implicit: columns }"> </ng-container>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { jalaliDiff } from '@/utils';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
computed,
|
||||||
ContentChild,
|
ContentChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -92,7 +93,7 @@ export interface IColumn<T = any> {
|
|||||||
export class PageDataListComponent<I = any> {
|
export class PageDataListComponent<I = any> {
|
||||||
constructor(
|
constructor(
|
||||||
private host: ElementRef,
|
private host: ElementRef,
|
||||||
private renderer: Renderer2,
|
private renderer: Renderer2
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Input({ required: true }) pageTitle!: string;
|
@Input({ required: true }) pageTitle!: string;
|
||||||
@@ -138,12 +139,16 @@ export class PageDataListComponent<I = any> {
|
|||||||
filterDrawerVisible = signal<boolean>(false);
|
filterDrawerVisible = signal<boolean>(false);
|
||||||
expandedRows: { [key: string]: boolean } = {};
|
expandedRows: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
|
hasCaption = computed(
|
||||||
|
() => !!(this.pageTitle || this.showAdd || this.filter || this.showRefresh || this.moreActions)
|
||||||
|
);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.height)
|
if (this.height)
|
||||||
this.renderer.setStyle(
|
this.renderer.setStyle(
|
||||||
this.host.nativeElement,
|
this.host.nativeElement,
|
||||||
'height',
|
'height',
|
||||||
this.fullHeight ? '100cqmin' : this.height,
|
this.fullHeight ? '100cqmin' : this.height
|
||||||
);
|
);
|
||||||
// if (this.fullHeight) {
|
// if (this.fullHeight) {
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -23,6 +23,5 @@ export const appRoutes: Routes = [
|
|||||||
path: 'auth',
|
path: 'auth',
|
||||||
loadComponent: () => import('@/modules/auth/pages/auth.component').then((m) => m.AuthComponent),
|
loadComponent: () => import('@/modules/auth/pages/auth.component').then((m) => m.AuthComponent),
|
||||||
},
|
},
|
||||||
{ path: 'notfound', component: Notfound },
|
{ path: '**', component: Notfound },
|
||||||
{ path: '**', redirectTo: '/notfound' },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { posAuthNamedRoutes } from '@/domains/pos/modules/auth/constants';
|
import { posAuthNamedRoutes } from '@/domains/pos/modules/auth/constants';
|
||||||
import { POS_ROUTES } from '@/domains/pos/routes';
|
import { POS_ROUTES } from '@/domains/pos/routes';
|
||||||
|
import { PUBLIC_SALE_INVOICES_ROUTES } from '@/modules/saleInvoices/constants';
|
||||||
import { Notfound } from '@/pages/notfound/notfound.component';
|
import { Notfound } from '@/pages/notfound/notfound.component';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
@@ -14,6 +15,6 @@ export const appRoutes: Routes = [
|
|||||||
...posAuthNamedRoutes.login,
|
...posAuthNamedRoutes.login,
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
},
|
},
|
||||||
{ path: 'notfound', component: Notfound },
|
...PUBLIC_SALE_INVOICES_ROUTES,
|
||||||
{ path: '**', redirectTo: '/notfound' },
|
{ path: '**', component: Notfound },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user