Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad1a73c16 | |||
| 8c07dc7c3f |
@@ -4,15 +4,251 @@
|
||||
|
||||
- This file defines repository-specific instructions for coding agents.
|
||||
- Scope is the full repo unless a deeper `AGENT.md` overrides it.
|
||||
- Use RTK to reduce token usage, avoid repeated context transfer, and preserve repository knowledge across sessions.
|
||||
|
||||
---
|
||||
|
||||
# ✅ 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
|
||||
|
||||
- Frontend: Angular 20 standalone app.
|
||||
- Package manager: `pnpm`.
|
||||
- Deployment: Docker / Docker Compose with tenant-specific services.
|
||||
- Current service mapping expectation:
|
||||
- `app_default` on host port `8090`
|
||||
- `app_tis` on host port `8091`
|
||||
- AI-assisted repository navigation: RTK.
|
||||
|
||||
### Current service mapping expectation
|
||||
|
||||
- `app_default` on host port `8090`
|
||||
- `app_tis` on host port `8091`
|
||||
|
||||
---
|
||||
|
||||
## Tenant Build Rules
|
||||
|
||||
@@ -23,6 +259,8 @@
|
||||
- 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`).
|
||||
@@ -31,46 +269,76 @@
|
||||
- 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'` or `type === 'price'`:
|
||||
- Normalize Persian/Arabic digits to English digits.
|
||||
- Allow only digits and `.`.
|
||||
- Support `fixed` precision formatting when provided.
|
||||
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`.
|
||||
- Do reuse `app-input` and set only required props (`type`, `label`, `name`, constraints).
|
||||
- Do register every new field in:
|
||||
### 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`
|
||||
- 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 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`.
|
||||
- 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:
|
||||
- `selector`: `field-<field-name>`
|
||||
- template: `<app-input ... />`
|
||||
- inputs: `control` (required), optional `name`, optional `label`
|
||||
|
||||
Example pattern:
|
||||
Minimal wrapper shape:
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
@@ -85,15 +353,19 @@ export class ExampleComponent {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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]`
|
||||
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:
|
||||
|
||||
@@ -104,55 +376,157 @@ example: (value = '', isRequired = true): ControlConfig => [
|
||||
],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Fields In Forms
|
||||
|
||||
- In form group builders, use `fieldControl.<key>(initialValue, isRequired)` for consistency.
|
||||
- In templates, render matching wrapper component and pass the matching control:
|
||||
- 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` `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
|
||||
|
||||
- Centralized list metadata is stored in `src/app/shared/constants/list-configs/` — **never** in domain modules.
|
||||
- Structure by data type, not domain (e.g. `good-list.const.ts`, `sku-list.const.ts`, `category-list.const.ts`).
|
||||
- Each config implements `IListConfig` (defined in `list-config.model.ts`):
|
||||
- `pageTitle`: display title for the list page
|
||||
- `addNewCtaLabel`: call-to-action button label
|
||||
- `emptyPlaceholderTitle` and `emptyPlaceholderDescription`: empty state messaging
|
||||
- `columns`: array of `IColumn[]` definitions
|
||||
- Usage in list components:
|
||||
```ts
|
||||
@Input() header: IColumn[] = goodListConfig.columns;
|
||||
listConfig = goodListConfig;
|
||||
```
|
||||
- Any domain needing a list config imports from `@/shared/constants/list-configs` — no cross-domain dependencies.
|
||||
- **Do not** define configs inside domains or duplicate them across modules.
|
||||
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() {
|
||||
|
||||
```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
|
||||
|
||||
- For TypeScript-only changes, run:
|
||||
- `pnpm -s exec tsc -p tsconfig.app.json --noEmit`
|
||||
- For Docker/build changes, verify with:
|
||||
- `docker compose build app_default`
|
||||
- `docker compose build app_tis`
|
||||
- Start with targeted validation, then broader checks only if needed.
|
||||
### 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 any assumptions or discovered config mismatches.
|
||||
- If validation is blocked (permissions, missing dependencies), state it clearly and provide next command.
|
||||
- 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.
|
||||
|
||||
@@ -209,6 +209,8 @@
|
||||
".webp": "file"
|
||||
},
|
||||
"styles": [
|
||||
"node_modules/flatpickr-wrap/dist/flatpickr.css",
|
||||
"node_modules/flatpickr-wrap/dist/themes/confetti.css",
|
||||
"src/assets/styles.scss"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json"
|
||||
@@ -250,6 +252,8 @@
|
||||
"inlineStyleLanguage": "scss",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"styles": [
|
||||
"node_modules/flatpickr-wrap/dist/flatpickr.css",
|
||||
"node_modules/flatpickr-wrap/dist/themes/confetti.css",
|
||||
"src/assets/styles.scss"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@tailwindcss/postcss": "^4.2.3",
|
||||
"@zoomit/dayjs-jalali-plugin": "^0.1.11",
|
||||
"angularx-qrcode": "20.0.0",
|
||||
"chart.js": "4.4.2",
|
||||
"dayjs": "^1.11.20",
|
||||
"flatpickr": "^4.6.13",
|
||||
|
||||
Generated
+314
-182
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 { PROVIDER_ROUTES } from '@/domains/provider/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 { Routes } from '@angular/router';
|
||||
|
||||
@@ -23,6 +24,6 @@ export const appRoutes: Routes = [
|
||||
path: 'auth',
|
||||
loadComponent: () => import('@/modules/auth/pages/auth.component').then((m) => m.AuthComponent),
|
||||
},
|
||||
{ path: 'notfound', component: Notfound },
|
||||
{ path: '**', redirectTo: '/notfound' },
|
||||
...PUBLIC_SALE_INVOICES_ROUTES,
|
||||
{ path: '**', component: Notfound },
|
||||
];
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import ISummary from '@/core/models/summary';
|
||||
import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/landing/models';
|
||||
import { TCustomerInfo, TPosOrderGoodPayload } from '@/domains/pos/modules/shop/models';
|
||||
|
||||
export interface ISalesInvoicesRawResponse {
|
||||
id: string;
|
||||
|
||||
@@ -5,11 +5,12 @@ import { RouterLink } from '@angular/router';
|
||||
import { Button } from 'primeng/button';
|
||||
import { Drawer } from 'primeng/drawer';
|
||||
import { Ripple } from 'primeng/ripple';
|
||||
import config from 'src/config';
|
||||
import { posAboutNamedRoutes } from '../../modules/about/constants';
|
||||
import { posConfigNamedRoutes } from '../../modules/configs/constants';
|
||||
import { PosGoodsManagementRoutes } from '../../modules/goodsManagement/constants';
|
||||
import { posSaleInvoicesNamedRoutes } from '../../modules/saleInvoices/constants';
|
||||
import { PosShopNamedRoutes } from '../../modules/shop/constants/routes';
|
||||
import { StatisticsRoutes } from '../../modules/statistics/constants';
|
||||
import { posSupportNamedRoutes } from '../../modules/support/constants';
|
||||
import { PosInfoStore } from '../../store';
|
||||
|
||||
@@ -37,10 +38,15 @@ export class PosMainMenuSidebarComponent extends AbstractDialog {
|
||||
|
||||
readonly menuItems = [
|
||||
{
|
||||
label: 'فروش',
|
||||
routerLink: config.isPosApplication ? '/' : '/pos',
|
||||
label: 'صفحه اصلی',
|
||||
routerLink: StatisticsRoutes.statistics.meta.pagePath!(),
|
||||
icon: 'pi pi-fw pi-home',
|
||||
},
|
||||
{
|
||||
label: 'فروش',
|
||||
routerLink: PosShopNamedRoutes.shop.meta.pagePath!(),
|
||||
icon: 'pi pi-fw pi-cart-arrow-down',
|
||||
},
|
||||
{
|
||||
label: 'فاکتورها',
|
||||
routerLink: posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!(),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Message } from 'primeng/message';
|
||||
import { catchError, finalize } from 'rxjs';
|
||||
import { COOKIE_KEYS } from 'src/assets/constants';
|
||||
import { IPosAccessibleResponse } from '../../models/pos.io';
|
||||
import { PosService } from '../../modules/landing/services/main.service';
|
||||
import { PosService } from '../../modules/shop/services/main.service';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-choose-cards',
|
||||
@@ -35,7 +35,7 @@ export class PosChooseCardsComponent {
|
||||
catchError((err) => {
|
||||
this.error.set(err);
|
||||
throw err;
|
||||
}),
|
||||
})
|
||||
)
|
||||
.subscribe((res) => {
|
||||
this.items.set(res?.data || []);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="w-full h-full flex items-center justify-center p-4">
|
||||
<div class="flex h-full w-full items-center justify-center p-4">
|
||||
<app-card-data cardTitle="تنظیمات فاکتور" class="w-full">
|
||||
<p-message variant="text" severity="info" icon="pi pi-info-circle">
|
||||
هریک از موارد زیر را که میخواهید در چاپ فاکتور نمایش داده شوند را انتخاب کنید
|
||||
</p-message>
|
||||
<pos-config-print-form class="block w-full mt-6" (onClose)="returnToMainPage()" (onSubmit)="returnToMainPage()" />
|
||||
<pos-config-print-form class="mt-6 block w-full" (onClose)="returnToMainPage()" (onSubmit)="returnToMainPage()" />
|
||||
</app-card-data>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppCardComponent } from '@/shared/components';
|
||||
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Message } from 'primeng/message';
|
||||
@@ -7,7 +8,7 @@ import { PosConfigPrintFormComponent } from '../components/print/form.component'
|
||||
@Component({
|
||||
selector: 'pos-config-page',
|
||||
templateUrl: './root.component.html',
|
||||
imports: [PosConfigPrintFormComponent, AppCardComponent, Message],
|
||||
imports: [PosConfigPrintFormComponent, AppCardComponent, Message, UikitFlatpickrJalaliComponent],
|
||||
})
|
||||
export class PosConfigPageComponent {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-lg">سفارش #{{ heldOrder.orderNumber }}</span>
|
||||
<span class="text-sm text-muted-color">
|
||||
{{ heldOrder.orderItems.length }} کالا -
|
||||
<span [appPriceMask]="heldOrder.totalAmount"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
pButton
|
||||
size="small"
|
||||
type="button"
|
||||
label="بارگذاری"
|
||||
icon="pi pi-download"
|
||||
(click)="loadHeldOrder(heldOrder.id)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
size="small"
|
||||
type="button"
|
||||
label="حذف"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
(click)="cancelHeldOrder(heldOrder.id)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,57 +0,0 @@
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { IPosHeldOrderResponse } from '@/modules/pos/models';
|
||||
import { ConfirmationDialogService } from '@/shared/components/confirmationDialog/confirmation-dialog.service';
|
||||
import { PriceMaskDirective } from '@/shared/directives';
|
||||
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { PosService } from '../../services/main.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-held-order-item',
|
||||
templateUrl: './held-order-item.component.html',
|
||||
imports: [PriceMaskDirective, ButtonDirective],
|
||||
})
|
||||
export class HeldOrderItemComponent {
|
||||
@Input() heldOrder!: IPosHeldOrderResponse;
|
||||
@Input() posId!: number;
|
||||
|
||||
@Output() onCancel = new EventEmitter<number>();
|
||||
@Output() onLoad = new EventEmitter<number>();
|
||||
|
||||
constructor(
|
||||
private readonly posService: PosService,
|
||||
private readonly confirmationService: ConfirmationDialogService,
|
||||
private readonly toastService: ToastService,
|
||||
) {}
|
||||
|
||||
actionLoading = signal<boolean>(false);
|
||||
|
||||
cancelHeldOrder(heldOrderId: number) {
|
||||
this.confirmationService.confirm({
|
||||
message: `آیا از لغو سفارش شمارهی #${this.heldOrder.orderNumber} اطمینان دارید؟`,
|
||||
header: 'لغو سفارش موقت',
|
||||
acceptLabel: 'بله',
|
||||
rejectLabel: 'خیر',
|
||||
accept: () => {
|
||||
this.actionLoading.set(true);
|
||||
// this.posService.cancelOrder(this.posId, heldOrderId).subscribe({
|
||||
// next: () => {
|
||||
// this.onCancel.emit(heldOrderId);
|
||||
// this.actionLoading.set(false);
|
||||
// this.toastService.success({
|
||||
// text: `سفارش شمارهی #${this.heldOrder.orderNumber} با موفقیت لغو شد.`,
|
||||
// });
|
||||
// },
|
||||
// error: () => {
|
||||
// this.actionLoading.set(false);
|
||||
// },
|
||||
// });
|
||||
},
|
||||
reject: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
loadHeldOrder(heldOrderId: number) {
|
||||
this.onLoad.emit(heldOrderId);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<div class="d-flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-2 text-muted-color">
|
||||
<i class="pi pi-shopping-cart"></i>
|
||||
<span class="text-base font-bold">سفارشهای نگهداشته شده</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (!heldOrders()?.length) {
|
||||
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
|
||||
<span class="text-base text-muted-color pt-4">هیچ سفارش نگهداشته شدهای وجود ندارد.</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex flex-col gap-2 mt-3">
|
||||
@for (heldOrder of heldOrders(); track heldOrder.id) {
|
||||
<app-held-order-item [heldOrder]="heldOrder" (onCancel)="removeHeldOrder($event)" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { IPosHeldOrderResponse } from '@/modules/pos/models';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { PosService } from '../../services/main.service';
|
||||
import { HeldOrderItemComponent } from './held-order-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-held-orders',
|
||||
templateUrl: './held-orders.component.html',
|
||||
imports: [HeldOrderItemComponent],
|
||||
})
|
||||
export class HeldOrdersComponent {
|
||||
private readonly service = inject(PosService);
|
||||
|
||||
heldOrders = signal<Maybe<IPosHeldOrderResponse[]>>(null);
|
||||
loading = signal<boolean>(false);
|
||||
|
||||
ngOnInit() {
|
||||
this.getHeldOrders();
|
||||
}
|
||||
|
||||
getHeldOrders() {
|
||||
this.loading.set(true);
|
||||
// this.service.getHeldOrders().subscribe({
|
||||
// next: (res) => {
|
||||
// this.heldOrders.set(res.data);
|
||||
// this.loading.set(false);
|
||||
// },
|
||||
// error: () => {
|
||||
// this.loading.set(false);
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
removeHeldOrder(orderId: number) {
|
||||
const currentOrders = this.heldOrders() || [];
|
||||
this.heldOrders.set(currentOrders.filter((order) => order.id !== orderId));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
|
||||
customer_mobile: this.fb.control<string>(''),
|
||||
customer_national_id: this.fb.control<string>(''),
|
||||
customer_economic_code: this.fb.control<string>(''),
|
||||
status: this.fb.control<string | null>(null),
|
||||
status: this.fb.control<'NOT_SEND' | 'SUCCESS' | 'FAILURE' | 'QUEUED' | null>(null),
|
||||
total_amount_from: this.fb.control<number | null>(null),
|
||||
total_amount_to: this.fb.control<number | null>(null),
|
||||
});
|
||||
@@ -74,7 +74,7 @@ export class PosSaleInvoicesFilterDrawerComponent implements OnChanges {
|
||||
total_amount_from: this.value.total_amount_from ?? null,
|
||||
total_amount_to: this.value.total_amount_to ?? null,
|
||||
},
|
||||
{ emitEvent: false },
|
||||
{ emitEvent: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { POS_ROUTES } from '@/domains/pos/routes';
|
||||
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
||||
import { gregorianToJalali } from '@/utils';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Button, ButtonDirective } from 'primeng/button';
|
||||
import { Chip } from 'primeng/chip';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
@@ -32,6 +33,7 @@ import { SaleInvoiceCardComponent } from './sale-invoice-card.component';
|
||||
export class PosSaleInvoiceListComponent {
|
||||
private readonly service = inject(PosSaleInvoicesService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
loading = signal(true);
|
||||
items = signal<IPosSaleInvoicesSummaryResponse[]>([]);
|
||||
@@ -57,16 +59,28 @@ export class PosSaleInvoiceListComponent {
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => {
|
||||
const typedKey = key as keyof IPosSaleInvoicesFilterDto;
|
||||
const formattedValue =
|
||||
typedKey === 'status' && value === 'NOT_SEND'
|
||||
let formattedValue = '';
|
||||
if (typedKey === 'status') {
|
||||
formattedValue =
|
||||
value === 'NOT_SEND'
|
||||
? 'ارسال نشده'
|
||||
: typedKey === 'status' && value === 'SUCCESS'
|
||||
: value === 'SUCCESS'
|
||||
? 'موفق'
|
||||
: typedKey === 'status' && value === 'FAILURE'
|
||||
: value === 'FAILURE'
|
||||
? 'ناموفق'
|
||||
: typedKey === 'status' && value === 'QUEUED'
|
||||
: value === 'QUEUED'
|
||||
? 'در انتظار ارسال'
|
||||
: String(value);
|
||||
} else if (
|
||||
key === 'invoice_date_from' ||
|
||||
key === 'invoice_date_to' ||
|
||||
key === 'created_at_from' ||
|
||||
key === 'created_at_to'
|
||||
) {
|
||||
formattedValue = gregorianToJalali(value);
|
||||
} else {
|
||||
formattedValue = String(value);
|
||||
}
|
||||
return { key: typedKey, label: labels[typedKey] ?? typedKey, value: formattedValue };
|
||||
});
|
||||
});
|
||||
@@ -74,6 +88,7 @@ export class PosSaleInvoiceListComponent {
|
||||
backRoute = POS_ROUTES.path || '/';
|
||||
|
||||
ngOnInit() {
|
||||
this.hydrateFiltersFromQueryParams();
|
||||
this.getData();
|
||||
}
|
||||
|
||||
@@ -96,6 +111,7 @@ export class PosSaleInvoiceListComponent {
|
||||
|
||||
onFilterApply(query: IPosSaleInvoicesFilterDto) {
|
||||
this.filters.set(query);
|
||||
this.syncFiltersToQueryParams();
|
||||
this.getData();
|
||||
}
|
||||
|
||||
@@ -103,10 +119,57 @@ export class PosSaleInvoiceListComponent {
|
||||
const next = { ...this.filters() };
|
||||
delete next[key];
|
||||
this.filters.set(next);
|
||||
this.syncFiltersToQueryParams();
|
||||
this.getData();
|
||||
}
|
||||
|
||||
toSinglePage(item: IPosSaleInvoicesResponse) {
|
||||
this.router.navigateByUrl(posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(item.id));
|
||||
}
|
||||
|
||||
private hydrateFiltersFromQueryParams() {
|
||||
const queryParams = this.route.snapshot.queryParams;
|
||||
const keys: (keyof IPosSaleInvoicesFilterDto)[] = [
|
||||
'invoice_date_from',
|
||||
'invoice_date_to',
|
||||
'created_at_from',
|
||||
'created_at_to',
|
||||
'code',
|
||||
'customer_name',
|
||||
'customer_mobile',
|
||||
'customer_national_id',
|
||||
'customer_economic_code',
|
||||
'status',
|
||||
'total_amount_from',
|
||||
'total_amount_to',
|
||||
];
|
||||
|
||||
const parsed = keys.reduce<IPosSaleInvoicesFilterDto>((acc, key) => {
|
||||
const value = queryParams[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.filters.set(parsed);
|
||||
}
|
||||
|
||||
private syncFiltersToQueryParams() {
|
||||
const queryParams = Object.entries(this.filters()).reduce<Record<string, string>>(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams,
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IPosSaleInvoicesFilterDto {
|
||||
customer_mobile?: string;
|
||||
customer_national_id?: string;
|
||||
customer_economic_code?: string;
|
||||
status?: string;
|
||||
status?: 'NOT_SEND' | 'SUCCESS' | 'FAILURE' | 'QUEUED';
|
||||
total_amount?: number | null;
|
||||
total_amount_from?: number | null;
|
||||
total_amount_to?: number | null;
|
||||
|
||||
+7
-5
@@ -4,13 +4,15 @@
|
||||
[modal]="true"
|
||||
[style]="{ 'max-height': '90svh', height: 'auto' }"
|
||||
[closable]="true"
|
||||
(onHide)="close()"
|
||||
>
|
||||
<div class="text-center pt-10 pb-14 flex flex-col items-center justify-center gap-4">
|
||||
<i class="" class="pi pi-check-circle text-6xl! text-green-700"></i>
|
||||
(onHide)="close()">
|
||||
<div class="flex flex-col items-center justify-center gap-4 pt-10 pb-14 text-center">
|
||||
<qrcode [qrdata]="publicInvoiceRoute()" [width]="220" [errorCorrectionLevel]="'M'"></qrcode>
|
||||
<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>
|
||||
</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)="sendInvoice()">ارسال به سامانه مودیان</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 { Component, computed, inject, Input, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { QRCodeComponent } from 'angularx-qrcode';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { finalize } from 'rxjs';
|
||||
import { posSaleInvoicesNamedRoutes } from '../../../saleInvoices/constants';
|
||||
@@ -15,7 +16,7 @@ import { IPosOrderResponse } from '../../models';
|
||||
@Component({
|
||||
selector: 'pos-order-submitted-dialog',
|
||||
templateUrl: 'order-submitted-dialog.component.html',
|
||||
imports: [SharedLightBottomsheetComponent, ButtonDirective],
|
||||
imports: [SharedLightBottomsheetComponent, ButtonDirective, QRCodeComponent],
|
||||
})
|
||||
export class PosOrderSubmittedDialogComponent extends AbstractDialog {
|
||||
@Input({ required: true }) invoice!: IPosOrderResponse;
|
||||
@@ -33,12 +34,16 @@ export class PosOrderSubmittedDialogComponent extends AbstractDialog {
|
||||
return '';
|
||||
});
|
||||
|
||||
readonly publicInvoiceRoute = computed(
|
||||
() => `${window.location.origin}/sale-invoices/${this.invoice.id}`
|
||||
);
|
||||
|
||||
sendingLoading = signal(false);
|
||||
sended = signal(false);
|
||||
|
||||
openOrderDetails() {
|
||||
this.router.navigateByUrl(
|
||||
posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.invoice.id),
|
||||
posSaleInvoicesNamedRoutes.saleInvoice.meta.pagePath!(this.invoice.id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import config from 'src/config';
|
||||
|
||||
export type TShopRouteNames = 'shop';
|
||||
|
||||
const baseRoute = `${config.isPosApplication ? '' : '/pos'}/shop`;
|
||||
|
||||
export const PosShopNamedRoutes: NamedRoutes<TShopRouteNames> = {
|
||||
shop: {
|
||||
path: 'shop',
|
||||
loadComponent: () => import('../../views/root.component').then((m) => m.PosShopComponent),
|
||||
meta: {
|
||||
title: 'فروشگاه',
|
||||
pagePath: () => baseRoute,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const POS_SHOP_ROUTES: Routes = [PosShopNamedRoutes.shop];
|
||||
+2
-2
@@ -14,7 +14,7 @@ import { IPosOrderResponse } from '../models';
|
||||
import { PosLandingStore } from '../store/main.store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-landing',
|
||||
selector: 'pos-shop',
|
||||
templateUrl: './root.component.html',
|
||||
host: { class: 'grow overflow-hidden' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -29,7 +29,7 @@ import { PosLandingStore } from '../store/main.store';
|
||||
PosOrderSubmittedDialogComponent,
|
||||
],
|
||||
})
|
||||
export class PosLandingComponent extends AbstractIsMobileComponent {
|
||||
export class PosShopComponent extends AbstractIsMobileComponent {
|
||||
private readonly infoStore = inject(PosInfoStore);
|
||||
private readonly landingStore = inject(PosLandingStore);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './invoice-type-card.component';
|
||||
@@ -0,0 +1,33 @@
|
||||
<div
|
||||
class="text-surface-700 relative flex flex-col gap-4 overflow-hidden rounded-2xl border px-4 py-4 pe-2"
|
||||
[style.backgroundColor]="preparedInfo().bgColor"
|
||||
[style.borderColor]="preparedInfo().borderColor"
|
||||
(click)="showDetails()">
|
||||
<div class="absolute top-0 right-0 h-full w-1" [style.backgroundColor]="preparedInfo().borderColor"></div>
|
||||
<div class="text-surface-700 flex items-center justify-between gap-2">
|
||||
<div class="text-surface-700 flex items-center gap-2">
|
||||
<span
|
||||
class="text-surface-700 flex h-8 w-8 items-center justify-center rounded-full border"
|
||||
[style.borderColor]="preparedInfo().borderColor"
|
||||
[style.backgroundColor]="preparedInfo().borderColor + '33'">
|
||||
<i class="text-sm" [ngClass]="'pi pi-' + preparedInfo().icon" [style.color]="preparedInfo().borderColor"></i>
|
||||
</span>
|
||||
<span class="text-surface-700 text-sm font-semibold">{{ preparedInfo().title }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="text-surface-700 rounded-full border px-2 py-1 text-xs font-bold"
|
||||
[style.borderColor]="preparedInfo().borderColor">
|
||||
{{ count || 0 }} عدد
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-surface-700 grid grid-cols-1 gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-surface-700/80 text-xs">مبلغ کل:</div>
|
||||
<div class="text-surface-700 mt-1 text-sm font-bold">{{ formattedTotalAmount }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-surface-700/80 text-xs">مالیات کل:</div>
|
||||
<div class="text-surface-700 mt-1 text-sm font-bold">{{ formattedTotalTax }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { formatWithCurrency } from '@/utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-statistics-invoice-type-card',
|
||||
templateUrl: 'invoice-type-card.component.html',
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class PosStatisticsInvoiceTypeCardComponent {
|
||||
@Input() type: 'all' | 'success' | 'failure' | 'notSended' | 'pending' | 'credit' = 'all';
|
||||
@Input() count?: number = 0;
|
||||
@Input() totalAmount?: number = 0;
|
||||
@Input() totalTaxAmount?: number = 0;
|
||||
@Input() loading = false;
|
||||
@Input() invoicesRoute = '';
|
||||
|
||||
@Output() onClick = new EventEmitter<void>();
|
||||
|
||||
get formattedTotalAmount() {
|
||||
return formatWithCurrency(this.totalAmount || 0);
|
||||
}
|
||||
get formattedTotalTax() {
|
||||
return formatWithCurrency(this.totalTaxAmount || 0);
|
||||
}
|
||||
|
||||
readonly preparedInfo = computed(() => {
|
||||
switch (this.type) {
|
||||
case 'all':
|
||||
return {
|
||||
title: 'همه',
|
||||
icon: 'close',
|
||||
bgColor: '#6B72801A',
|
||||
borderColor: '#6B7280',
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
title: 'تایید شده',
|
||||
icon: 'check',
|
||||
bgColor: '#22C55E1A',
|
||||
borderColor: '#22C55E',
|
||||
};
|
||||
case 'failure':
|
||||
return {
|
||||
title: 'خطادار',
|
||||
icon: 'times',
|
||||
bgColor: '#EF44441A',
|
||||
borderColor: '#EF4444',
|
||||
};
|
||||
case 'notSended':
|
||||
return {
|
||||
title: 'ارسال نشده',
|
||||
icon: 'clock',
|
||||
bgColor: '#F59E0B1A',
|
||||
borderColor: '#F59E0B',
|
||||
};
|
||||
case 'pending':
|
||||
return {
|
||||
title: 'انتظار ارسال',
|
||||
icon: 'clock',
|
||||
bgColor: '#3B82F61A',
|
||||
borderColor: '#3B82F6',
|
||||
};
|
||||
case 'credit':
|
||||
return {
|
||||
title: 'اعتبار',
|
||||
icon: 'credit-card',
|
||||
bgColor: '#8B5CF61A',
|
||||
borderColor: '#8B5CF6',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
showDetails() {
|
||||
this.onClick.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const baseUrl = () => `/api/v1/pos/statistics`;
|
||||
|
||||
export const POS_STATISTICS_API_ROUTES = {
|
||||
getAll: () => `${baseUrl()}/sale-invoices`,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './apiRoutes/index';
|
||||
export * from './routes/index';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import config from 'src/config';
|
||||
|
||||
export type TStatisticsRouteNames = 'statistics';
|
||||
|
||||
const baseRoute = `${config.isPosApplication ? '' : '/pos'}`;
|
||||
|
||||
export const StatisticsRoutes: NamedRoutes<TStatisticsRouteNames> = {
|
||||
statistics: {
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('../../views/root.component').then((m) => m.PosStatisticsRootComponent),
|
||||
meta: {
|
||||
title: 'داشبودر',
|
||||
pagePath: () => baseRoute,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const POS_STATISTICS_ROUTES: Routes = [StatisticsRoutes.statistics];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './io';
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface IPosStatisticsRawResponse {
|
||||
all: IPosStatisticsItem;
|
||||
success: IPosStatisticsItem;
|
||||
failure: IPosStatisticsItem;
|
||||
pending: IPosStatisticsItem;
|
||||
notSended: IPosStatisticsItem;
|
||||
credit: IPosStatisticsItem;
|
||||
}
|
||||
export interface IPosStatisticsResponse extends IPosStatisticsRawResponse {}
|
||||
|
||||
interface IPosStatisticsItem {
|
||||
count: number;
|
||||
total_amount: number;
|
||||
total_tax: number;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { POS_STATISTICS_API_ROUTES } from '../constants';
|
||||
import { IPosStatisticsRawResponse, IPosStatisticsResponse } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PosStatisticsService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
private apiRoutes = POS_STATISTICS_API_ROUTES;
|
||||
|
||||
getAll(fromDate?: Date): Observable<IPosStatisticsResponse> {
|
||||
return this.http.get<IPosStatisticsRawResponse>(this.apiRoutes.getAll(), {
|
||||
params: { from_date: fromDate?.toISOString() || '' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { defaultBaseStateData, EntityState, EntityStore } from '@/core/state';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { catchError, finalize } from 'rxjs';
|
||||
import { IPosStatisticsResponse } from '../models';
|
||||
import { PosStatisticsService } from '../services/main.service';
|
||||
|
||||
interface PosStatisticsState extends EntityState<IPosStatisticsResponse> {
|
||||
lastFromDate: Date;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PosStatisticsStore extends EntityStore<IPosStatisticsResponse, PosStatisticsState> {
|
||||
private readonly service = inject(PosStatisticsService);
|
||||
constructor() {
|
||||
super({
|
||||
...defaultBaseStateData,
|
||||
lastFromDate: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
getData(fromDate?: Date) {
|
||||
if (fromDate && fromDate.getTime() === this._state().lastFromDate.getTime()) {
|
||||
return;
|
||||
}
|
||||
this.patchState({ loading: true, lastFromDate: fromDate || new Date() });
|
||||
this.service
|
||||
.getAll(fromDate)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.patchState({ loading: false });
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.setError(error);
|
||||
throw error;
|
||||
})
|
||||
)
|
||||
.subscribe((entity) => {
|
||||
this.patchState({ entity });
|
||||
});
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this.setState({
|
||||
...defaultBaseStateData,
|
||||
lastFromDate: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './root.component';
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="flex flex-col gap-6 p-4">
|
||||
<season-picker (onChange)="onDateChanged($event)" />
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@if (loading()) {
|
||||
@for (_ of [0, 1, 2, 3, 4, 5]; track $index) {
|
||||
<div class="h-32 w-full">
|
||||
<p-skeleton width="100%" height="100%"></p-skeleton>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<pos-statistics-invoice-type-card
|
||||
type="all"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.all?.total_amount"
|
||||
[totalTaxAmount]="item()?.all?.total_tax"
|
||||
[count]="item()?.all?.count"
|
||||
(onClick)="toInvoicesPage('all')" />
|
||||
<pos-statistics-invoice-type-card
|
||||
type="success"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.success?.total_amount"
|
||||
[totalTaxAmount]="item()?.success?.total_tax"
|
||||
[count]="item()?.success?.count"
|
||||
(onClick)="toInvoicesPage('success')" />
|
||||
<pos-statistics-invoice-type-card
|
||||
type="failure"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.failure?.total_amount"
|
||||
[totalTaxAmount]="item()?.failure?.total_tax"
|
||||
[count]="item()?.failure?.count"
|
||||
(onClick)="toInvoicesPage('failure')" />
|
||||
<pos-statistics-invoice-type-card
|
||||
type="notSended"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.notSended?.total_amount"
|
||||
[totalTaxAmount]="item()?.notSended?.total_tax"
|
||||
[count]="item()?.notSended?.count"
|
||||
(onClick)="toInvoicesPage('notSended')" />
|
||||
<pos-statistics-invoice-type-card
|
||||
type="pending"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.pending?.total_amount"
|
||||
[totalTaxAmount]="item()?.pending?.total_tax"
|
||||
[count]="item()?.pending?.count"
|
||||
(onClick)="toInvoicesPage('pending')" />
|
||||
<pos-statistics-invoice-type-card
|
||||
type="credit"
|
||||
[loading]="loading()"
|
||||
[totalAmount]="item()?.credit?.total_amount"
|
||||
[totalTaxAmount]="item()?.credit?.total_tax"
|
||||
[count]="item()?.credit?.count" />
|
||||
}
|
||||
</div>
|
||||
<div class="fixed inset-x-0 bottom-0 p-4">
|
||||
<button pButton [routerLink]="shopRoute" size="large" class="w-full">ایجاد صورتحساب جدید</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { SeasonPickerComponent } from '@/shared/components/seasonPicker/season-picker.component';
|
||||
import { formatGregorian, gregorianToJalali, parseJalali } from '@/utils';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import { posSaleInvoicesNamedRoutes } from '../../saleInvoices/constants';
|
||||
import { IPosSaleInvoicesFilterDto } from '../../saleInvoices/models';
|
||||
import { PosShopNamedRoutes } from '../../shop/constants/routes';
|
||||
import { PosStatisticsInvoiceTypeCardComponent } from '../components';
|
||||
import { PosStatisticsStore } from '../store/main.store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-statistics-root',
|
||||
templateUrl: './root.component.html',
|
||||
imports: [
|
||||
PosStatisticsInvoiceTypeCardComponent,
|
||||
Skeleton,
|
||||
SeasonPickerComponent,
|
||||
ButtonDirective,
|
||||
RouterLink,
|
||||
],
|
||||
})
|
||||
export class PosStatisticsRootComponent {
|
||||
private readonly store = inject(PosStatisticsStore);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly item = computed(() => this.store.entity());
|
||||
readonly loading = computed(() => this.store.loading());
|
||||
readonly shopRoute = PosShopNamedRoutes.shop.meta.pagePath!();
|
||||
|
||||
selectedDate = signal(new Date());
|
||||
|
||||
ngOnInit() {
|
||||
this.getData(this.selectedDate());
|
||||
}
|
||||
|
||||
onDateChanged(date: Date) {
|
||||
this.selectedDate.set(date);
|
||||
this.getData(date);
|
||||
}
|
||||
|
||||
getData(date?: Date) {
|
||||
this.store.getData(date);
|
||||
}
|
||||
|
||||
toInvoicesPage(type: string) {
|
||||
const mainRoute = posSaleInvoicesNamedRoutes.saleInvoices.meta.pagePath!();
|
||||
const queryParams: IPosSaleInvoicesFilterDto = {
|
||||
invoice_date_from: this.selectedDate().toISOString(),
|
||||
invoice_date_to: new Date(
|
||||
formatGregorian(
|
||||
parseJalali(gregorianToJalali(this.selectedDate())).add(3, 'months').set('day', 1)
|
||||
)
|
||||
).toISOString(),
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
queryParams.status = 'SUCCESS';
|
||||
break;
|
||||
case 'failure':
|
||||
queryParams.status = 'FAILURE';
|
||||
break;
|
||||
case 'notSended':
|
||||
queryParams.status = 'NOT_SEND';
|
||||
break;
|
||||
case 'pending':
|
||||
queryParams.status = 'QUEUED';
|
||||
break;
|
||||
}
|
||||
const queryString = new URLSearchParams(queryParams as Record<string, string>).toString();
|
||||
|
||||
this.router.navigateByUrl(`${mainRoute}?${queryString}`);
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,20 @@ import { POS_ABOUT_ROUTES } from './modules/about/constants';
|
||||
import { POS_CONFIG_ROUTES } from './modules/configs/constants';
|
||||
import { POS_GOODS_MANAGEMENT_ROUTES } from './modules/goodsManagement/constants';
|
||||
import { POS_SALE_INVOICES_ROUTES } from './modules/saleInvoices/constants';
|
||||
import { POS_SHOP_ROUTES } from './modules/shop/constants/routes';
|
||||
import { POS_STATISTICS_ROUTES } from './modules/statistics/constants';
|
||||
import { POS_SUPPORT_ROUTES } from './modules/support/constants';
|
||||
|
||||
export const POS_ROUTES = {
|
||||
path: '',
|
||||
loadComponent: () => import('@/layout/default/app.layout.component').then((m) => m.AppLayout),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./modules/landing/views/root.component').then((m) => m.PosLandingComponent),
|
||||
},
|
||||
...POS_SHOP_ROUTES,
|
||||
...POS_SALE_INVOICES_ROUTES,
|
||||
...POS_ABOUT_ROUTES,
|
||||
...POS_SUPPORT_ROUTES,
|
||||
...POS_CONFIG_ROUTES,
|
||||
...POS_GOODS_MANAGEMENT_ROUTES,
|
||||
...POS_STATISTICS_ROUTES,
|
||||
],
|
||||
} as Route;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { catchError, finalize, map } from 'rxjs';
|
||||
import { IPosProfileResponse } from '../models';
|
||||
import { PosService } from '../modules/landing/services/main.service';
|
||||
import { PosService } from '../modules/shop/services/main.service';
|
||||
|
||||
interface PosProfileState extends EntityState<IPosProfileResponse> {}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class PosProfileStore extends EntityStore<IPosProfileResponse, PosProfile
|
||||
this.setEntity(entity);
|
||||
}
|
||||
return entity;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CookieService } from 'ngx-cookie-service';
|
||||
import { catchError, finalize, map } from 'rxjs';
|
||||
import { COOKIE_KEYS } from 'src/assets/constants';
|
||||
import { IPosInfoResponse } from '../models/pos.io';
|
||||
import { PosService } from '../modules/landing/services/main.service';
|
||||
import { PosService } from '../modules/shop/services/main.service';
|
||||
|
||||
interface PosState extends EntityState<IPosInfoResponse> {
|
||||
posId: string;
|
||||
@@ -44,7 +44,7 @@ export class PosInfoStore extends EntityStore<IPosInfoResponse, PosState> {
|
||||
this.setEntity(entity);
|
||||
}
|
||||
return entity;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user