Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c39472dca | ||
|
|
53dfd20913 | ||
|
|
8feaee61cb | ||
|
|
e5766ea311 | ||
|
|
a4fd166c51 | ||
|
|
47eac3badc | ||
|
|
9698228c51 | ||
|
|
9ab254ef0d | ||
|
|
b0081c7031 | ||
|
|
e12ae120b0 | ||
|
|
e4573ac0c9 | ||
|
|
4570495a77 | ||
|
|
bdc2d9f1b4 | ||
|
|
a1cac84e2b | ||
|
|
aa73b9aed2 | ||
|
|
39bcfe2817 | ||
|
|
94fe19c658 | ||
|
|
0c0450b152 | ||
|
|
2540d52670 | ||
|
|
071293b60f | ||
|
|
1c019d12f3 | ||
|
|
3fdd681dd0 | ||
|
|
2ace5dbb83 | ||
|
|
2ad57f0ad9 | ||
|
|
052677fa2c | ||
|
|
42c8c59947 | ||
|
|
14acee1127 | ||
|
|
3bf3405c48 | ||
|
|
e72bdfd170 | ||
|
|
5a83040077 | ||
|
|
69279fc341 | ||
|
|
65dd0e2e56 | ||
|
|
c2a74b6764 | ||
|
|
f2c7156851 | ||
|
|
6093003b70 | ||
|
|
0c28735c90 | ||
|
|
350239b307 | ||
|
|
b42a4c0777 | ||
|
|
9c75788ba7 | ||
|
|
c1ecd5ea50 | ||
|
|
99a4087099 | ||
|
|
5c28080e9b | ||
|
|
ffa5de8311 | ||
|
|
3f28fde39f | ||
|
|
1b901efc51 | ||
|
|
42f403a7ed | ||
|
|
827fd13fa7 | ||
|
|
771ebfc308 | ||
|
|
b154a8de33 | ||
|
|
51d67ef7c2 | ||
|
|
fd01ebf7c7 | ||
|
|
a85d24235c | ||
|
|
721653f692 | ||
|
|
c0cc880be2 | ||
|
|
0650cf6297 | ||
|
|
04a886ea26 | ||
|
|
5be6c54272 | ||
|
|
0a484e0b45 | ||
|
|
90ee59d388 | ||
|
|
9ae41e4f2c | ||
|
|
edee45375e | ||
|
|
56683ee7b9 | ||
|
|
357414c247 | ||
|
|
dff22d75b5 | ||
|
|
b8f13e36b3 | ||
|
|
eae0dee1f2 | ||
|
|
1d2406483e | ||
|
|
8148850fda | ||
|
|
b8cd39eaa4 | ||
|
|
89b39cad38 | ||
|
|
67545d9295 |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.webp binary
|
||||
*.pdf binary
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -87,6 +87,7 @@ web_modules/
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
public/generated/
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
@@ -136,3 +137,4 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
storage/organizations/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npx lint-staged
|
||||
@@ -1 +0,0 @@
|
||||
bun run build
|
||||
181
AGENTS.md
181
AGENTS.md
@@ -34,6 +34,127 @@ Current migration status:
|
||||
|
||||
---
|
||||
|
||||
## Governance Layer
|
||||
|
||||
This repository now treats governance documentation as implementation input, not optional background reading.
|
||||
|
||||
Before any task that changes behavior, architecture, permissions, reporting, exports, PDFs, approvals, or CRM workflow, agents must review the relevant standards and foundations first.
|
||||
|
||||
Primary governance documents:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/standards/task-contract-template.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/architecture-rules.md`
|
||||
- `docs/standards/ui-ux-rules.md`
|
||||
- `docs/standards/task-review-checklist.md`
|
||||
- `docs/adr/**`
|
||||
- `docs/business/**`
|
||||
- `docs/security/**`
|
||||
- `docs/implementation/**` for the related feature lineage
|
||||
|
||||
### Task Execution Rules
|
||||
|
||||
All tasks follow these rules:
|
||||
|
||||
- review first; implementation without review is prohibited
|
||||
- reuse existing foundations before creating new modules, helpers, or flows
|
||||
- extend existing services before creating duplicate services
|
||||
- extend existing route groups before creating duplicate APIs
|
||||
- extend existing permission models before creating duplicate permission systems
|
||||
- extend existing export/report foundations before creating duplicate export flows
|
||||
- keep business logic in feature service layers, not in route handlers or client components
|
||||
- document any exception when an existing foundation cannot be reused
|
||||
|
||||
Required review order for non-trivial work:
|
||||
|
||||
1. `AGENTS.md`
|
||||
2. `docs/standards/**`
|
||||
3. relevant `docs/adr/**`
|
||||
4. relevant `docs/business/**`
|
||||
5. related completed tasks from `docs/standards/task-catalog.md` and `docs/implementation/**`
|
||||
6. existing foundations under `src/features/foundation/**`
|
||||
7. existing feature implementations under `src/features/**`
|
||||
8. existing APIs under `src/app/api/**`
|
||||
9. permission and access enforcement under `src/lib/auth/**`, `src/features/crm/security/**`, and `docs/security/**`
|
||||
10. existing audit patterns under `src/features/foundation/audit-log/**`
|
||||
|
||||
### Historical Knowledge Rule
|
||||
|
||||
Before implementing any feature:
|
||||
|
||||
1. review related foundations
|
||||
2. review related ADRs
|
||||
3. review related completed tasks from `docs/standards/task-catalog.md`
|
||||
4. reuse before creating
|
||||
|
||||
Implementation without historical review is prohibited.
|
||||
|
||||
### Required Foundations
|
||||
|
||||
The following reusable foundations are mandatory whenever the task touches their area:
|
||||
|
||||
- Audit Foundation: `src/features/foundation/audit-log/**`
|
||||
- Approval Foundation: `src/features/foundation/approval/**`
|
||||
- Storage Foundation: `src/features/foundation/storage/**` and `src/features/foundation/document-artifact/**`
|
||||
- PDF Foundation: `src/features/foundation/pdf-generator/**` and `src/features/crm/quotations/document/**`
|
||||
- Report Foundation: `src/features/crm/reports/**`, `docs/adr/0017-report-foundation.md`
|
||||
- CRM Authorization Foundation: `src/lib/auth/crm-access.ts`, `src/features/crm/security/**`, `docs/security/**`
|
||||
- Customer Ownership Foundation: `docs/adr/0015-customer-ownership-contact-sharing.md`, `src/features/crm/customers/**`
|
||||
- Contact Sharing Foundation: `docs/adr/0015-customer-ownership-contact-sharing.md`, `src/app/api/crm/customers/[id]/contacts/[contactId]/shares/**`
|
||||
|
||||
If a task touches CRM leads, enquiries, quotations, approvals, reports, exports, storage, or PDFs, agents must explicitly check whether one of these foundations already solves part of the problem.
|
||||
|
||||
### Architecture Constraints
|
||||
|
||||
Frontend constraints:
|
||||
|
||||
- Next.js App Router only
|
||||
- server components first
|
||||
- `PageContainer` for dashboard page headers
|
||||
- shadcn/ui primitives and existing app UI wrappers
|
||||
- TanStack React Query with server prefetch + `HydrationBoundary` + client `useSuspenseQuery()` for data-heavy app pages
|
||||
- TanStack Form + Zod for forms
|
||||
- `nuqs` for URL state
|
||||
- existing CRM terminology helpers and Thai labels for business-facing CRM UI
|
||||
|
||||
Backend constraints:
|
||||
|
||||
- Route Handlers are the main HTTP boundary
|
||||
- feature service layer owns business logic
|
||||
- Drizzle ORM owns database access
|
||||
- auth and organization access go through `@/auth` and `src/lib/auth/session.ts`
|
||||
- CRM scope resolution goes through resolved-access helpers, not direct role-string branching
|
||||
- audit logging goes through `src/features/foundation/audit-log/service.ts`
|
||||
|
||||
Forbidden patterns:
|
||||
|
||||
- Redux for new application state
|
||||
- SWR for new server-state fetching
|
||||
- React Hook Form for new forms in this repo
|
||||
- direct database access from client components
|
||||
- business logic inside route handlers
|
||||
- new Clerk usage
|
||||
- direct role-string authorization such as `if (role === 'sales')`
|
||||
- report-specific export systems that bypass `src/features/crm/reports/server/exports/service.ts`
|
||||
|
||||
### Mandatory Task Review
|
||||
|
||||
Before implementation, every non-trivial task must review:
|
||||
|
||||
- reusable foundations
|
||||
- relevant ADRs
|
||||
- related completed tasks from `docs/standards/task-catalog.md`
|
||||
- related existing APIs
|
||||
- permission and scope enforcement
|
||||
- audit events and existing entity/action naming
|
||||
- duplication risk across services, routes, exports, datasets, and UI shells
|
||||
|
||||
Implementation without this review is prohibited.
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack Details
|
||||
|
||||
### Core Runtime
|
||||
@@ -291,6 +412,22 @@ Notes:
|
||||
|
||||
---
|
||||
|
||||
## Date & Time Formatting Rules
|
||||
|
||||
- All date and time rendering must use the shared formatter in `src/lib/date-format.ts`.
|
||||
- Never use `Date.prototype.toLocaleString()`, `toLocaleDateString()`, `toLocaleTimeString()`, or `Intl.DateTimeFormat()` directly inside React components.
|
||||
- UI date display standard:
|
||||
- Date: `YYYY-MM-DD`
|
||||
- DateTime: `YYYY-MM-DD HH:mm:ss`
|
||||
- Time: `HH:mm:ss`
|
||||
- API responses must remain ISO-8601 UTC.
|
||||
- UI is responsible for formatting.
|
||||
- Timezone is fixed to `Asia/Bangkok`.
|
||||
- Calendar system must always use Gregorian (AD), never Buddhist Era (BE).
|
||||
- All formatting must be deterministic to ensure identical SSR and Client rendering and prevent React hydration mismatches.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Patterns
|
||||
|
||||
### Protected Routes
|
||||
@@ -375,6 +512,31 @@ Preferred pattern for new pages:
|
||||
3. hydrate with `HydrationBoundary`
|
||||
4. read with `useSuspenseQuery()`
|
||||
|
||||
Required pattern for CRUD mutations:
|
||||
|
||||
1. every feature must define centralized query key factories in `queries.ts`
|
||||
2. use grouped keys such as `all`, `lists()`, `list(filters)`, `details()`, `detail(id)`, plus child-resource keys like `contacts(id)` or `followups(id)` when needed
|
||||
3. after create, explicitly invalidate the list-level key
|
||||
4. after update, explicitly invalidate both list and detail keys
|
||||
5. after delete, explicitly invalidate the list key and remove stale detail queries with `removeQueries`
|
||||
6. invalidate related queries affected by the mutation, such as counts, child tabs, document previews, approval panels, and cross-feature related lists
|
||||
7. do not rely on `staleTime` alone to refresh production CRUD data
|
||||
8. do not rely on `router.refresh()` alone for CRUD freshness; React Query invalidation is required first, and `router.refresh()` is optional only as a supplement
|
||||
|
||||
Mutation callback safety rule:
|
||||
|
||||
- if a shared mutation object from `api/mutations.ts` is spread into `useMutation({ ...sharedMutation, ... })`, do not override `onSuccess` or `onSettled` in a way that drops cache invalidation
|
||||
- prefer keeping cache invalidation inside the shared mutation definition
|
||||
- if component-level success behavior is needed, either:
|
||||
- put invalidation in shared `onSettled` and keep UI concerns like toast and closing dialogs in component `onSuccess`, or
|
||||
- call the shared invalidate helper explicitly from the component before closing the sheet/dialog
|
||||
|
||||
UI completion rule after successful mutation:
|
||||
|
||||
- close sheet/dialog only after the mutation promise resolves successfully
|
||||
- table/detail views must show fresh data immediately without a full browser refresh
|
||||
- related tabs and counts must refresh in the same interaction when their backing data changed
|
||||
|
||||
### URL State
|
||||
|
||||
Use `nuqs` for table search params and list filters.
|
||||
@@ -486,3 +648,22 @@ Ensure these are set:
|
||||
12. Treat legacy mock-backed modules as migration seams.
|
||||
13. Follow the migrated `products` and `users` feature patterns when building new CRUD modules.
|
||||
14. Do not re-enable self-service sign-up unless the product requirement changes explicitly.
|
||||
15. For every CRUD mutation, explicitly invalidate React Query caches for list/detail/related data; never rely on passive staleness recovery.
|
||||
16. Centralize query keys in each feature's `api/queries.ts`; do not scatter manual string query keys in components.
|
||||
17. When deleting entities, remove stale detail queries with `removeQueries` in addition to invalidating lists.
|
||||
18. If a component overrides `useMutation` callbacks on top of a shared mutation config, preserve or explicitly call the shared invalidation behavior before closing UI.
|
||||
19. After successful create/update/delete, sheets, dialogs, and destructive modals must close only after cache invalidation has been triggered and the UI can refresh from fresh data.
|
||||
20. Review `docs/standards/**`, relevant `docs/adr/**`, `docs/business/**`, and `docs/security/**` before implementing non-trivial changes.
|
||||
21. Review related completed task notes in `docs/standards/task-catalog.md` and `docs/implementation/**` before implementing non-trivial changes.
|
||||
22. Reuse foundations under `src/features/foundation/**` and `src/features/crm/**` before introducing new services, routes, permissions, exports, or datasets.
|
||||
23. Do not create duplicate report/export/PDF/approval/security plumbing when a project foundation already exists.
|
||||
24. CRM authorization must flow through resolved access helpers such as `resolveCrmMembershipAccess()`, `buildCrmSecurityContext()`, or report-context builders; do not authorize with raw role strings alone.
|
||||
|
||||
Before implementing any task, review:
|
||||
- AGENTS.md
|
||||
- docs/standards/*
|
||||
- docs/standards/task-catalog.md
|
||||
- related ADRs
|
||||
- related completed task documents
|
||||
|
||||
Implementation without review is prohibited.
|
||||
|
||||
21
CLAUDE.md
21
CLAUDE.md
@@ -1,21 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This is a Next.js 16 + shadcn/ui admin dashboard starter kit.
|
||||
|
||||
## Key References
|
||||
|
||||
- **[AGENTS.md](./AGENTS.md)** — Full project overview, tech stack, structure, conventions, data fetching patterns, deployment
|
||||
- **[docs/forms.md](./docs/forms.md)** — Form system: TanStack Form + Zod, composable fields, validation, multi-step, sheet/dialog forms
|
||||
- **[docs/themes.md](./docs/themes.md)** — Theme system: OKLCH colors, adding themes, font config
|
||||
- **[docs/nav-rbac.md](./docs/nav-rbac.md)** — Navigation RBAC: access control, Clerk integration
|
||||
- **[docs/clerk_setup.md](./docs/clerk_setup.md)** — Clerk auth setup: organizations, billing, environment variables
|
||||
|
||||
## Critical Conventions
|
||||
|
||||
- **React Query** for all data fetching — `void prefetchQuery()` on server + `useSuspenseQuery` on client (standard TanStack pattern), `useMutation` for forms, `HydrationBoundary` + `dehydrate` for hydration, `<Suspense fallback>` for streaming
|
||||
- **API layer** per feature — `api/types.ts` → `api/service.ts` → `api/queries.ts`; queries use key factories (`entityKeys.all/list/detail`); components import from service and queries, never from mock APIs directly
|
||||
- **nuqs** for URL search params — `searchParamsCache` on server, `useQueryStates` on client, use `getSortingStateParser` for sort (same parser as `useDataTable`)
|
||||
- **Icons** — only import from `@/components/icons`, never from `@tabler/icons-react` directly
|
||||
- **Forms** — use `useAppForm` + `useFormFields<T>()` from `@/components/ui/tanstack-form`
|
||||
- **Page headers** — use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`), never import `<Heading>` manually
|
||||
- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent
|
||||
15
artifacts/pdf-audit/payload-parity-report.json
Normal file
15
artifacts/pdf-audit/payload-parity-report.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"audit": "payload-parity",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"previewHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26",
|
||||
"generationHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26",
|
||||
"snapshotHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26",
|
||||
"mismatches": [],
|
||||
"comparableFieldCount": 31,
|
||||
"topicInputKeyCount": 6,
|
||||
"signatureParity": true,
|
||||
"priceTableParity": true,
|
||||
"priceTableRowCount": 1,
|
||||
"priceTableCurrency": "THB"
|
||||
}
|
||||
18
artifacts/pdf-audit/placeholder-integrity-report.json
Normal file
18
artifacts/pdf-audit/placeholder-integrity-report.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"audit": "placeholder-integrity",
|
||||
"overallStatus": "PASS",
|
||||
"results": [
|
||||
{
|
||||
"templateName": "ALLA Demo Quotation Standard",
|
||||
"organizationId": "daf796fc-4588-5240-a27e-9bfef0739658",
|
||||
"version": "1.0",
|
||||
"duplicateFieldNames": [],
|
||||
"typoFields": [],
|
||||
"unmappedPlaceholders": [],
|
||||
"deadMappings": [],
|
||||
"invalidReservedMappings": [],
|
||||
"missingReservedFields": [],
|
||||
"status": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
1187
artifacts/pdf-audit/runtime-payload-report.json
Normal file
1187
artifacts/pdf-audit/runtime-payload-report.json
Normal file
File diff suppressed because it is too large
Load Diff
14
artifacts/pdf-audit/signature-audit-report.json
Normal file
14
artifacts/pdf-audit/signature-audit-report.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"audit": "approved-snapshot-parity",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"approvedSnapshotAvailable": true,
|
||||
"approvedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84",
|
||||
"computedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84",
|
||||
"inconsistentTemplateKeys": [],
|
||||
"computedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||
"storedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||
"approvedArtifactReference": null,
|
||||
"refreshedFixtureSnapshot": true,
|
||||
"usedAuditFixture": false
|
||||
}
|
||||
28
artifacts/pdf-audit/summary.json
Normal file
28
artifacts/pdf-audit/summary.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"generatedAt": "2026-06-30T16:18:58.393Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"artifacts": {
|
||||
"templateVersionReport": "template-version-report.json",
|
||||
"placeholderIntegrityReport": "placeholder-integrity-report.json",
|
||||
"runtimePayloadReport": "runtime-payload-report.json",
|
||||
"payloadParityReport": "payload-parity-report.json",
|
||||
"topicRuntimeReport": "topic-runtime-report.json",
|
||||
"signatureAuditReport": "signature-audit-report.json"
|
||||
},
|
||||
"statuses": {
|
||||
"templateVersion": "PASS",
|
||||
"placeholderIntegrity": "PASS",
|
||||
"runtimePayload": "PASS",
|
||||
"payloadParity": "PASS",
|
||||
"topicRuntime": "PASS",
|
||||
"approvedSnapshotParity": "PASS",
|
||||
"staticLabels": "PASS"
|
||||
},
|
||||
"priceTable": {
|
||||
"present": true,
|
||||
"rowCount": 1,
|
||||
"parity": true,
|
||||
"status": "PASS"
|
||||
}
|
||||
}
|
||||
27
artifacts/pdf-audit/summary.md
Normal file
27
artifacts/pdf-audit/summary.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# PDF Integrity Summary
|
||||
|
||||
- Generated At: 2026-06-30T16:18:58.393Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: CRA2606-1108
|
||||
|
||||
## Statuses
|
||||
|
||||
- Template Version Integrity: PASS
|
||||
- Placeholder Integrity: PASS
|
||||
- Runtime Payload: PASS
|
||||
- Payload Parity: PASS
|
||||
- Topic Runtime: PASS
|
||||
- Approved Snapshot Parity: PASS
|
||||
- Static Labels: PASS
|
||||
- Price Table Present: Yes
|
||||
- Price Table Status: PASS
|
||||
- Price Table Parity: Yes
|
||||
|
||||
## Artifact Files
|
||||
|
||||
- template-version-report.json
|
||||
- placeholder-integrity-report.json
|
||||
- runtime-payload-report.json
|
||||
- payload-parity-report.json
|
||||
- topic-runtime-report.json
|
||||
- signature-audit-report.json
|
||||
9
artifacts/pdf-audit/template-version-report.json
Normal file
9
artifacts/pdf-audit/template-version-report.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"audit": "template-version-integrity",
|
||||
"overallStatus": "PASS",
|
||||
"activeTemplateCount": 1,
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"approvedTemplateVersionId": null,
|
||||
"approvedTemplateVersionIsActive": false,
|
||||
"issues": []
|
||||
}
|
||||
18
artifacts/pdf-audit/topic-runtime-report.json
Normal file
18
artifacts/pdf-audit/topic-runtime-report.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"audit": "topic-runtime",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"topicCount": 3,
|
||||
"topicInputKeys": [
|
||||
"topic_1_0",
|
||||
"topic_1_1",
|
||||
"topic_1_2"
|
||||
],
|
||||
"itemTopicInputKeys": [
|
||||
"item_topic_1_0",
|
||||
"item_topic_1_1",
|
||||
"item_topic_1_2"
|
||||
],
|
||||
"duplicateKeys": [],
|
||||
"emptySections": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-e-hundred-items",
|
||||
"fixtureName": "quotation-100-items",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-100-items/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-100-items/ALLA.manifest.json",
|
||||
"pdfHash": "2d3348b1babd0ffc3f4f45909662c383c461a1c821d40255da97d89de31a0789",
|
||||
"runtimeHash": "b7b687d9ac008e500bf0f37a24535fe4388dea5a6588958fc9050d41f108f0cb",
|
||||
"pageCount": 9,
|
||||
"fileSize": 193308,
|
||||
"itemCount": 100,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 642.76,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-100-items/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-100-items/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-e-hundred-items",
|
||||
"fixtureName": "quotation-100-items",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-100-items/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-100-items/ONVALLA.manifest.json",
|
||||
"pdfHash": "97ab02049d7c6fb05205d0a06863e018d9011328207df86aa748ce5ca4be644b",
|
||||
"runtimeHash": "ccdbb0d85d8a1a225ae7570fa402148accebab23787f91793136ff7be121b258",
|
||||
"pageCount": 9,
|
||||
"fileSize": 182422,
|
||||
"itemCount": 100,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 764.57,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-100-items/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-100-items/ONVALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-d-twenty-items",
|
||||
"fixtureName": "quotation-20-items",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-20-items/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-20-items/ALLA.manifest.json",
|
||||
"pdfHash": "28b70e1008cd214fd9137c77fb46352a94e2e21a2d7439126a7e2cf24fd7e38e",
|
||||
"runtimeHash": "811fb45b60ce299b9743a716618579dba1838bc21289c221fee03bd4c8ed0d33",
|
||||
"pageCount": 4,
|
||||
"fileSize": 141752,
|
||||
"itemCount": 20,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 672.98,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-20-items/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-20-items/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-d-twenty-items",
|
||||
"fixtureName": "quotation-20-items",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-20-items/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-20-items/ONVALLA.manifest.json",
|
||||
"pdfHash": "5291f4855b4a09e722b83f3474f63e14a2446172101438740e0220c722fe35fa",
|
||||
"runtimeHash": "37768c99c829ddeeec9b6cb1241513010f1843b527baacad11feb81d79b4325a",
|
||||
"pageCount": 4,
|
||||
"fileSize": 131297,
|
||||
"itemCount": 20,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 485.69,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-20-items/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-20-items/ONVALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-h-performance-500",
|
||||
"fixtureName": "quotation-500-items",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-500-items/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-500-items/ALLA.manifest.json",
|
||||
"pdfHash": "404528f7104ad3713541228ecf56942867f074ef1a63232661d5719d1a19739b",
|
||||
"runtimeHash": "fbc778a2b6f6a30e29b948595ce4f7853b23561393d82f473a2a311bb360b71f",
|
||||
"pageCount": 36,
|
||||
"fileSize": 453800,
|
||||
"itemCount": 500,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 1394.5,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-500-items/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-500-items/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-a-empty",
|
||||
"fixtureName": "quotation-empty",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-empty/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-empty/ALLA.manifest.json",
|
||||
"pdfHash": "c0e14168ffc2e6cb531fe0b7e16fd6beb03486dcf716275501c02b9b02054fe8",
|
||||
"runtimeHash": "eead267464f1d36e4a8d3ef5ea13764c25f6003ebf70bd2d18eec131a4734b61",
|
||||
"pageCount": 3,
|
||||
"fileSize": 128488,
|
||||
"itemCount": 0,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 0,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 1057.33,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-empty/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-empty/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-a-empty",
|
||||
"fixtureName": "quotation-empty",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-empty/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-empty/ONVALLA.manifest.json",
|
||||
"pdfHash": "bb3ad97c1b3b684cfed4f970a3055f4b0fa2b43be04bbf0a83b4e2d55327753f",
|
||||
"runtimeHash": "434f42ae97434e50bb3f496372a08720fa11a462c79a612335a1ccd2173d8281",
|
||||
"pageCount": 3,
|
||||
"fileSize": 117606,
|
||||
"itemCount": 0,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 0,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 624.21,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-empty/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-empty/ONVALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-f-long-description",
|
||||
"fixtureName": "quotation-long-description",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-long-description/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-long-description/ALLA.manifest.json",
|
||||
"pdfHash": "e91ac6676bba513f17d4d31f2085d28d336b9ee314668f0d56fb66740d7b3656",
|
||||
"runtimeHash": "47730bfd52e890342bf8cae15d1a2c899deaa5aa0ce1c9bf0ecebc132918b408",
|
||||
"pageCount": 3,
|
||||
"fileSize": 145496,
|
||||
"itemCount": 4,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 519.4,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-long-description/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-long-description/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-f-long-description",
|
||||
"fixtureName": "quotation-long-description",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-long-description/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-long-description/ONVALLA.manifest.json",
|
||||
"pdfHash": "3695672bc38c1ed46713b2cfb73375fbbf2543bbd267d71baa37ecf0c9e619b7",
|
||||
"runtimeHash": "a04d9f634a7683b34c624d7eff0754d7cfd1f7a0c84de229eb8101e1afe2449b",
|
||||
"pageCount": 3,
|
||||
"fileSize": 133549,
|
||||
"itemCount": 4,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 507.75,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-g-multi-topic",
|
||||
"fixtureName": "quotation-multi-topic",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-multi-topic/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-multi-topic/ALLA.manifest.json",
|
||||
"pdfHash": "58afe01d030072e20dfbba3dde1e3d0b759c6265778bf4552bd6bcb467e63cfe",
|
||||
"runtimeHash": "e06b3c768c555ceba300774eee13e3addce0f1c31424c13146ed844b388a3e9a",
|
||||
"pageCount": 7,
|
||||
"fileSize": 149418,
|
||||
"itemCount": 4,
|
||||
"topicCount": 16,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 559.37,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-multi-topic/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-multi-topic/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-g-multi-topic",
|
||||
"fixtureName": "quotation-multi-topic",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-multi-topic/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-multi-topic/ONVALLA.manifest.json",
|
||||
"pdfHash": "caac2aa060b349bec41b7b50d24ff7f67ffdaac4c350bf7a05783bb55fc50f17",
|
||||
"runtimeHash": "7778eaf87354240732c7db4d5d2745c9ee12d0782937abe245010a4377d804a7",
|
||||
"pageCount": 7,
|
||||
"fileSize": 137115,
|
||||
"itemCount": 4,
|
||||
"topicCount": 16,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 468.28,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-multi-topic/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-multi-topic/ONVALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-b-single-item",
|
||||
"fixtureName": "quotation-single-item",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-single-item/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-single-item/ALLA.manifest.json",
|
||||
"pdfHash": "648de1590217e375cadd57a0f84fe615910f9ce00e354a68f1230773fa44e3b5",
|
||||
"runtimeHash": "21dc98b9480a7413b02212da478e7e781f8ccdf20441f1945b96a14d21e4f890",
|
||||
"pageCount": 3,
|
||||
"fileSize": 129396,
|
||||
"itemCount": 1,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 633.9,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-single-item/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-single-item/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-b-single-item",
|
||||
"fixtureName": "quotation-single-item",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-single-item/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-single-item/ONVALLA.manifest.json",
|
||||
"pdfHash": "f0138ae8b819e08573b28eee025ecac264c6dfd30d13e5e2f023038716ccf32c",
|
||||
"runtimeHash": "44e7e5a0d664026128fcfa5c6685d511e3e7e9f554b85790f122b1986f73936f",
|
||||
"pageCount": 3,
|
||||
"fileSize": 118794,
|
||||
"itemCount": 1,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 405.66,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-single-item/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-single-item/ONVALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-c-standard",
|
||||
"fixtureName": "quotation-standard",
|
||||
"organizationName": "ALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-standard/ALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-standard/ALLA.manifest.json",
|
||||
"pdfHash": "6e2efd618457c7a54266a3df308c96b07299b82a2a05b513d2b686aa9bf0abbe",
|
||||
"runtimeHash": "182e3f60ebf057f2eb6cc7e5fcc085a2de89997e971ef34bd17b7f77b46fea2e",
|
||||
"pageCount": 3,
|
||||
"fileSize": 140005,
|
||||
"itemCount": 4,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 1047.33,
|
||||
"templateFieldCount": 57,
|
||||
"templateFilePath": "src/pdfme_template/ALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "01db011220d800d94e9ee484fb55764c88dec7c65f98b1074c53c989e6823847"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-standard/ALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-standard/ALLA.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"datasetKey": "dataset-c-standard",
|
||||
"fixtureName": "quotation-standard",
|
||||
"organizationName": "ONVALLA",
|
||||
"pdfPath": "artifacts/pdf-visual/current/quotation-standard/ONVALLA.pdf",
|
||||
"manifestPath": "artifacts/pdf-visual/current/quotation-standard/ONVALLA.manifest.json",
|
||||
"pdfHash": "34d16603728a500c42a7d24433b88b4ea19f3080db4605b8eece21858e63c985",
|
||||
"runtimeHash": "f1fe7f8efc32670ada4b93f34aac1897da04c7e7667cabe5f5eacc36a813a99d",
|
||||
"pageCount": 3,
|
||||
"fileSize": 128510,
|
||||
"itemCount": 4,
|
||||
"topicCount": 3,
|
||||
"renderedSectionOrder": [
|
||||
"customer",
|
||||
"product_items",
|
||||
"topics"
|
||||
],
|
||||
"sectionAnchors": {
|
||||
"customer": 0,
|
||||
"product_items": 1,
|
||||
"topics": 2,
|
||||
"signature": 2
|
||||
},
|
||||
"firstItemsRowColumns": 7,
|
||||
"signatureOnLastPage": true,
|
||||
"runtimeIssues": [],
|
||||
"generationMs": 635.88,
|
||||
"templateFieldCount": 56,
|
||||
"templateFilePath": "src/pdfme_template/ONVALLA_template_pdfme_product_v1.json",
|
||||
"templateSourceHash": "aa03f71a363db0bbaf987b01fe815ed1731ec191cba4236fecd37cc1d3bbf162"
|
||||
}
|
||||
BIN
artifacts/pdf-visual/current/quotation-standard/ONVALLA.pdf
Normal file
BIN
artifacts/pdf-visual/current/quotation-standard/ONVALLA.pdf
Normal file
Binary file not shown.
1165
artifacts/pdf-visual/visual-regression-report.json
Normal file
1165
artifacts/pdf-visual/visual-regression-report.json
Normal file
File diff suppressed because it is too large
Load Diff
56
artifacts/pdf-visual/visual-regression-report.md
Normal file
56
artifacts/pdf-visual/visual-regression-report.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# PDF Visual Regression Report
|
||||
|
||||
- Generated At: 2026-06-29T09:34:12.489Z
|
||||
- Mode: baseline
|
||||
- Template Variant: product-v1
|
||||
- Overall Status: PASS
|
||||
|
||||
## Fixture Summary
|
||||
|
||||
| Fixture | Org | Pages | Items | Topics | PDF Hash | Runtime Hash |
|
||||
| --- | --- | ---: | ---: | ---: | --- | --- |
|
||||
| quotation-empty | ALLA | 3 | 0 | 3 | c0e14168ffc2 | eead267464f1 |
|
||||
| quotation-empty | ONVALLA | 3 | 0 | 3 | bb3ad97c1b3b | 434f42ae9743 |
|
||||
| quotation-single-item | ALLA | 3 | 1 | 3 | 648de1590217 | 21dc98b9480a |
|
||||
| quotation-single-item | ONVALLA | 3 | 1 | 3 | f0138ae8b819 | 44e7e5a0d664 |
|
||||
| quotation-standard | ALLA | 3 | 4 | 3 | 6e2efd618457 | 182e3f60ebf0 |
|
||||
| quotation-standard | ONVALLA | 3 | 4 | 3 | 34d16603728a | f1fe7f8efc32 |
|
||||
| quotation-20-items | ALLA | 4 | 20 | 3 | 28b70e1008cd | 811fb45b60ce |
|
||||
| quotation-20-items | ONVALLA | 4 | 20 | 3 | 352d5c1439ff | 37768c99c829 |
|
||||
| quotation-100-items | ALLA | 9 | 100 | 3 | 2d3348b1babd | b7b687d9ac00 |
|
||||
| quotation-100-items | ONVALLA | 9 | 100 | 3 | 97ab02049d7c | ccdbb0d85d8a |
|
||||
| quotation-long-description | ALLA | 3 | 4 | 3 | 59c77e9cbf29 | 47730bfd52e8 |
|
||||
| quotation-long-description | ONVALLA | 3 | 4 | 3 | 3695672bc38c | a04d9f634a76 |
|
||||
| quotation-multi-topic | ALLA | 7 | 4 | 16 | 58afe01d0300 | e06b3c768c55 |
|
||||
| quotation-multi-topic | ONVALLA | 7 | 4 | 16 | caac2aa060b3 | 7778eaf87354 |
|
||||
|
||||
## Comparison Summary
|
||||
|
||||
| Dataset | Org | Status | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| dataset-a-empty | ALLA | PASS | All automated checks passed |
|
||||
| dataset-a-empty | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-b-single-item | ALLA | PASS | All automated checks passed |
|
||||
| dataset-b-single-item | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-c-standard | ALLA | PASS | All automated checks passed |
|
||||
| dataset-c-standard | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-d-twenty-items | ALLA | PASS | All automated checks passed |
|
||||
| dataset-d-twenty-items | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-e-hundred-items | ALLA | PASS | All automated checks passed |
|
||||
| dataset-e-hundred-items | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-f-long-description | ALLA | PASS | All automated checks passed |
|
||||
| dataset-f-long-description | ONVALLA | PASS | All automated checks passed |
|
||||
| dataset-g-multi-topic | ALLA | PASS | All automated checks passed |
|
||||
| dataset-g-multi-topic | ONVALLA | PASS | All automated checks passed |
|
||||
|
||||
## Screenshot Strategy
|
||||
|
||||
- Baseline PDFs and manifests are stored under `fixtures/pdf-visual/**`.
|
||||
- Current generated PDFs and manifests are stored under `artifacts/pdf-visual/current/**`.
|
||||
- Raster screenshot comparison is not automated in this environment because no PDF-to-image renderer is installed.
|
||||
|
||||
## Performance
|
||||
|
||||
- 500-item fixture pages: 36
|
||||
- 500-item fixture generation time: 1394.5 ms
|
||||
- 500-item fixture PDF size: 453800 bytes
|
||||
BIN
artifacts/task-p4-verification/rows_30_short.pdf
Normal file
BIN
artifacts/task-p4-verification/rows_30_short.pdf
Normal file
Binary file not shown.
18
docs/adr/0007-quotation-revision-strategy.md
Normal file
18
docs/adr/0007-quotation-revision-strategy.md
Normal file
@@ -0,0 +1,18 @@
|
||||
Status:
|
||||
accepted
|
||||
|
||||
Context:
|
||||
Quotation revision was introduced in Task E, but the rules before approval workflow were still ambiguous.
|
||||
Task E.1 needs a stable rule so Task F can build approval on top without changing quotation lineage again.
|
||||
|
||||
Decision:
|
||||
- Revision is allowed only when the current quotation status is `approved`, `sent`, `accepted`, or `rejected`.
|
||||
- Revision is not allowed for `draft` or `cancelled`.
|
||||
- A new revision keeps the same quotation family using `parentQuotationId`.
|
||||
- A new revision copies header, items, customers, and topics only.
|
||||
- A new revision is created with the `revised` status when that master option exists.
|
||||
|
||||
Consequences:
|
||||
- Sales can revise commercially meaningful documents without reopening early drafts.
|
||||
- Approval work can later key off a stable quotation family and revision chain.
|
||||
- Follow-ups and attachments are still out of the revision snapshot and remain technical debt until explicitly designed.
|
||||
15
docs/adr/0008-attachment-storage-strategy.md
Normal file
15
docs/adr/0008-attachment-storage-strategy.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Status:
|
||||
accepted
|
||||
|
||||
Context:
|
||||
Task E introduced quotation attachments as metadata only.
|
||||
Task E.1 confirms that approval should not start on top of an unstable file-storage design.
|
||||
|
||||
Decision:
|
||||
- Keep quotation attachments metadata-only for now.
|
||||
- Store file identity, original name, path, type, size, uploader, and timestamps in CRM tables.
|
||||
- Do not build upload transport, storage provider logic, signed download URLs, or binary retention policy in Task E or E.1.
|
||||
|
||||
Consequences:
|
||||
- Task F can reference attachment metadata without being blocked by storage implementation details.
|
||||
- A later storage task must define provider abstraction, permission checks, retention rules, and migration strategy for existing metadata rows.
|
||||
32
docs/adr/0009-approved-document-storage-lifecycle.md
Normal file
32
docs/adr/0009-approved-document-storage-lifecycle.md
Normal file
@@ -0,0 +1,32 @@
|
||||
Status:
|
||||
accepted
|
||||
|
||||
Context:
|
||||
Task H introduced server-side PDF generation for quotation preview, download, and approved-document persistence.
|
||||
Task H.1 validated the full path with a real quotation fixture and confirmed that approved artifacts originally persisted to local disk under `public/generated/...`.
|
||||
Task I introduces a storage-provider abstraction and a database-backed document artifact model so approved PDFs are no longer tied to raw public paths as the source of truth.
|
||||
|
||||
Decision:
|
||||
|
||||
- Use a storage provider abstraction for approved quotation PDFs.
|
||||
- Support a local provider for development and an S3 / MinIO-compatible provider for production-oriented deployments.
|
||||
- Persist approved-document metadata in `crm_document_artifacts`.
|
||||
- Persist `approved_artifact_id` on the quotation row as the primary approved-artifact reference.
|
||||
- Keep `approved_pdf_url` as a compatibility field and allow `artifact:<artifactId>` references during the migration period.
|
||||
- Lock approved PDF artifacts after generation and treat them as immutable-first records.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Approved PDF persistence no longer depends on raw `public/generated/...` paths as the system of record.
|
||||
- Secure download routes can resolve artifacts from database metadata and storage-provider reads.
|
||||
- Local storage remains acceptable for development, but production-oriented deployments can move to S3/MinIO-compatible object storage without changing the quotation flow contract.
|
||||
- Legacy approved PDFs stored under `public/generated/...` still require explicit migration and remain a temporary compatibility burden.
|
||||
|
||||
Future:
|
||||
|
||||
- migrate legacy approved PDFs into `crm_document_artifacts`
|
||||
- extend the storage abstraction to manual attachments as well as generated artifacts
|
||||
- support signed URLs or another private delivery strategy where required
|
||||
- define operator-facing void/replacement workflow for immutable approved artifacts
|
||||
- add retention/cleanup policy for superseded or voided artifacts
|
||||
- move approved-PDF generation to an async job when post-approval auto-generation is introduced
|
||||
36
docs/adr/0010-revenue-attribution-governance.md
Normal file
36
docs/adr/0010-revenue-attribution-governance.md
Normal file
@@ -0,0 +1,36 @@
|
||||
Status:
|
||||
accepted
|
||||
|
||||
Context:
|
||||
Task D.2 introduced separate `Billing Customer` and `Project Parties` data entry in CRM enquiries and quotations.
|
||||
Task J.0 froze KPI definitions and confirmed that dashboard/reporting behavior must not drift without an explicit governance decision.
|
||||
Task D.2.1 freezes how revenue attribution and relationship reporting work across dashboard, report center, exports, and future analytics features.
|
||||
|
||||
Decision:
|
||||
|
||||
- Freeze `Revenue Owner = End Customer`.
|
||||
- Determine `Revenue Owner` from project-party role code `end_customer`.
|
||||
- If a record has no `end_customer`, fallback to `billing_customer` for revenue-owner attribution only.
|
||||
- Freeze `Billing Revenue` as quotation revenue grouped by project-party role `billing_customer`.
|
||||
- Freeze `Contractor Revenue` as quotation revenue grouped by project-party role `contractor`.
|
||||
- Freeze `Consultant Revenue` as quotation revenue grouped by project-party role `consultant`.
|
||||
- Use project-party relationships as the reporting authority, not bare `crm_customers` references alone.
|
||||
- Prefer quotation project parties for reporting. If quotation project parties are absent, fallback to enquiry project parties for the linked enquiry.
|
||||
- When multiple parties share the same reporting role on one quotation, each party receives full quotation attribution.
|
||||
- Do not prorate relationship analytics across multiple parties because this model is for CRM relationship reporting, not accounting allocation.
|
||||
- Exclude cancelled quotations from revenue attribution by default unless an explicit reporting filter intentionally includes them.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Dashboard KPI, report center, exports, and ad hoc analytics can share one attribution rule set.
|
||||
- `Top End Customers`, `Top Contractors`, `Top Consultants`, and `Top Billing Customers` can be built from the same reusable service layer.
|
||||
- A single quotation may contribute full value to more than one customer in relationship analytics when multiple parties share a role.
|
||||
- Revenue attribution and accounting allocation remain intentionally separate concerns.
|
||||
- Legacy or incomplete quotations without project-party rows may not attribute revenue until project-party data is synchronized or backfilled.
|
||||
|
||||
Future:
|
||||
|
||||
- add a unified reporting view or materialized projection if revenue analytics queries become heavy
|
||||
- add explicit lifecycle timestamps for won/lost stage reporting precision
|
||||
- decide whether approved snapshots should become the long-term reporting source for historically locked documents
|
||||
- add backfill tooling if legacy quotations are found without authoritative project-party rows
|
||||
131
docs/adr/0011-lead-enquiry-ownership-model.md
Normal file
131
docs/adr/0011-lead-enquiry-ownership-model.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# ADR 0011: Lead and Enquiry Ownership Model
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The CRM module originally used a mixed terminology model where:
|
||||
|
||||
- `Lead` meant an unassigned enquiry
|
||||
- `Opportunity` meant an assigned enquiry
|
||||
|
||||
That model no longer matched the business process. The business now distinguishes ownership by function:
|
||||
|
||||
- `Lead` is marketing-owned
|
||||
- `Enquiry` is sales-owned
|
||||
|
||||
The system must preserve the single-source-of-truth table `crm_enquiries` while refining lifecycle, permissions, monitoring visibility, and KPI semantics.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Single record model
|
||||
|
||||
- `crm_enquiries` remains the single source of truth
|
||||
- no `crm_leads` table is introduced
|
||||
- no split lead form or split enquiry API is introduced
|
||||
|
||||
### 2. Pipeline stage model
|
||||
|
||||
`crm_enquiries.pipeline_stage` is the lifecycle discriminator with allowed values:
|
||||
|
||||
- `lead`
|
||||
- `enquiry`
|
||||
- `closed_won`
|
||||
- `closed_lost`
|
||||
|
||||
### 3. Ownership model
|
||||
|
||||
- `lead` is marketing-owned
|
||||
- `enquiry` is sales-owned
|
||||
- assignment converts a lead into an enquiry
|
||||
|
||||
### 4. Assignment behavior
|
||||
|
||||
On assign:
|
||||
|
||||
- `assignedToUserId` is set
|
||||
- `assignedAt` is set
|
||||
- `assignedBy` is set
|
||||
- `pipelineStage` becomes `enquiry`
|
||||
|
||||
On reassign:
|
||||
|
||||
- assignee metadata is updated
|
||||
- `pipelineStage` remains `enquiry`
|
||||
|
||||
### 5. Create behavior
|
||||
|
||||
- marketing-created records default to `pipelineStage = lead`
|
||||
- sales-created records default to `pipelineStage = enquiry`
|
||||
|
||||
### 6. Closure behavior
|
||||
|
||||
- lost or cancelled enquiry status resolves to `pipelineStage = closed_lost`
|
||||
- accepted or lost/rejected quotation outcomes may synchronize the linked enquiry to:
|
||||
- `closed_won`
|
||||
- `closed_lost`
|
||||
|
||||
### 7. Visibility policy
|
||||
|
||||
Marketing can monitor:
|
||||
|
||||
- leads
|
||||
- enquiries
|
||||
- follow-ups
|
||||
- status
|
||||
- pipeline stage
|
||||
- enquiry owner
|
||||
- won/lost result
|
||||
|
||||
Marketing cannot access:
|
||||
|
||||
- quotation pricing
|
||||
- quotation items
|
||||
- cost, discount, or margin views
|
||||
- approval analytics or approval workflow data
|
||||
|
||||
Sales scope remains ownership-based:
|
||||
|
||||
- `sales` and `sales_support` are limited server-side to enquiries they created or are assigned to
|
||||
- managers and admins retain wider organization visibility
|
||||
|
||||
### 8. KPI terminology
|
||||
|
||||
User-facing dashboard terminology replaces `Opportunity` with `Enquiry`.
|
||||
|
||||
Count definitions:
|
||||
|
||||
- `Lead Count` = `pipelineStage = lead`
|
||||
- `Enquiry Count` = `pipelineStage = enquiry`
|
||||
- `Won Count` = `pipelineStage = closed_won`
|
||||
- `Lost Count` = `pipelineStage = closed_lost`
|
||||
|
||||
### 9. Navigation Separation
|
||||
|
||||
- `Lead` and `Enquiry` are separate UX workspaces
|
||||
- `Lead` and `Enquiry` are not separate domain entities
|
||||
- both workspaces continue to use the same `crm_enquiries` records
|
||||
- `pipelineStage` determines whether a record appears in the Leads or Enquiries workspace
|
||||
- the navigation split reduces user confusion without duplicating schema, API, or service layers
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- matches the real business handoff from marketing to sales
|
||||
- keeps backward-compatible storage in `crm_enquiries`
|
||||
- removes ambiguous `opportunity` wording from user-facing CRM surfaces
|
||||
- gives marketing monitoring visibility without exposing commercial data
|
||||
- enforces own-or-assigned visibility server-side for sales roles
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- lifecycle still depends partly on status mapping and quotation outcome synchronization
|
||||
- the model does not yet include dedicated timestamps such as `closedWonAt` or `closedLostAt`
|
||||
- reporting still relies on current-state lifecycle fields rather than effective-dated snapshots
|
||||
|
||||
## Supersedes
|
||||
|
||||
This ADR supersedes the opportunity terminology frozen in `docs/implementation/task-j0-kpi-definition-freeze.md` for user-facing CRM ownership and KPI language.
|
||||
63
docs/adr/0012-approval-signature-strategy.md
Normal file
63
docs/adr/0012-approval-signature-strategy.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# ADR 0012: Approval Signature Strategy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Quotation PDF templates already contain the signature placeholders `app1`, `app2`, `app3` and their position fields, but earlier tasks only seeded safe fallback values. The production document flow now needs a deterministic rule for:
|
||||
|
||||
- who is shown as `Prepared By`
|
||||
- who is shown as `Approved By`
|
||||
- who is shown as `Authorized By`
|
||||
- how each person’s position is resolved
|
||||
- how approved documents stay immutable after approval
|
||||
|
||||
## Decision
|
||||
|
||||
### Prepared By
|
||||
|
||||
- resolve from `crm_quotations.salesman_id`
|
||||
- fallback to `crm_quotations.created_by`
|
||||
- always render the user display name, never a raw id
|
||||
- use quotation `created_at` as the available timestamp for the prepared block
|
||||
|
||||
### Approved By
|
||||
|
||||
- resolve from live approval history in `crm_approval_actions`
|
||||
- consider only `approve` actions
|
||||
- choose the latest approved action whose workflow role is one of:
|
||||
- `sales_manager`
|
||||
- `department_manager`
|
||||
- exclude the workflow’s final approval role from this slot
|
||||
|
||||
### Authorized By
|
||||
|
||||
- resolve from the latest approved action for the workflow’s final step
|
||||
- in the standard workflow this is expected to be `top_manager`
|
||||
|
||||
### Position Resolution
|
||||
|
||||
- primary source is `memberships.business_role`
|
||||
- display label should come from active `ms_options` in category `crm_job_title` when available
|
||||
- if the master option is missing, fallback labels are:
|
||||
- `sales` -> `Sales Engineer`
|
||||
- `sales_support` -> `Sales Support`
|
||||
- `sales_manager` -> `Sales Manager`
|
||||
- `department_manager` -> `Department Manager`
|
||||
- `top_manager` -> `General Manager`
|
||||
|
||||
### Snapshot Immutability
|
||||
|
||||
- preview PDF resolves signatures from live quotation and approval data
|
||||
- approved snapshot stores the resolved signature block and resolved template input
|
||||
- approved PDF generation prefers snapshot signature input when a snapshot already exists
|
||||
- locked approved artifacts remain the immutable historical source
|
||||
|
||||
## Consequences
|
||||
|
||||
- signature placeholder mapping is now explicit and organization-aware
|
||||
- template output no longer depends on hardcoded placeholder text in the PDFME template
|
||||
- position labels can be governed centrally through master options without changing generator code
|
||||
- approved documents preserve approval identity even if user names, memberships, or master options change later
|
||||
49
docs/adr/0013-pdf-visual-parity-strategy.md
Normal file
49
docs/adr/0013-pdf-visual-parity-strategy.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ADR 0013: PDF Visual Parity Strategy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The quotation PDF flow now includes:
|
||||
|
||||
- production PDFME templates per organization
|
||||
- DB-backed mapping registry
|
||||
- dynamic topic expansion
|
||||
- approval signature resolution
|
||||
- approved snapshot persistence
|
||||
|
||||
What was missing was a repeatable audit strategy proving that template schema, mappings, runtime payload, and approved snapshot content remain synchronized over time.
|
||||
|
||||
## Decision
|
||||
|
||||
- freeze a documented PDF mapping registry in `docs/business/pdf-mapping-registry.md`
|
||||
- keep a human-readable parity checklist in `docs/implementation/pdf-parity-checklist.md`
|
||||
- add an audit suite:
|
||||
- `audit-pdf-template-inventory`
|
||||
- `audit-pdf-mapping-coverage`
|
||||
- `audit-pdf-runtime-payload`
|
||||
- `generate-pdf-audit-report`
|
||||
- expose the suite through `npm run audit:pdf`
|
||||
- prefer the stable fixture quotation code `QT-H5-AUDIT` for repeatable audits when available
|
||||
- compare preview runtime content against approved snapshot content for business-field consistency
|
||||
|
||||
## Visual Parity Rules
|
||||
|
||||
- allowed differences:
|
||||
- watermark state
|
||||
- generated timestamp
|
||||
- artifact metadata
|
||||
- disallowed differences:
|
||||
- customer identity
|
||||
- quotation commercial values
|
||||
- topic content
|
||||
- signature identity
|
||||
- mapped table content
|
||||
|
||||
## Consequences
|
||||
|
||||
- regressions in template coverage become detectable without opening every PDF manually
|
||||
- mapping ownership is explicit and reviewable
|
||||
- future PDF changes must update both runtime logic and the registry/checklist when business content changes
|
||||
72
docs/adr/0014-crm-multi-role-user-assignment.md
Normal file
72
docs/adr/0014-crm-multi-role-user-assignment.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# ADR 0014: CRM Multi-Role User Assignment
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Task L introduced CRM role profiles and resolved CRM access, but authorization still depended on a single `memberships.businessRole` value per organization membership.
|
||||
|
||||
That model is too restrictive for real CRM operations because one user may need multiple CRM responsibilities inside the same organization, such as:
|
||||
|
||||
- `sales` + `sales_manager`
|
||||
- `crm_admin` + `department_manager`
|
||||
- `marketing` + `sales_support`
|
||||
|
||||
We need to keep `memberships` as organization access while moving CRM authorization into its own persistent model.
|
||||
|
||||
## Decision
|
||||
|
||||
We adopt:
|
||||
|
||||
- `memberships` as organization/workspace access
|
||||
- `crm_user_role_assignments` as CRM authorization
|
||||
|
||||
One user can have many CRM role assignments per organization.
|
||||
|
||||
Each assignment stores:
|
||||
|
||||
- `roleProfileId`
|
||||
- branch scope mode and branch IDs
|
||||
- product-type scope mode and product-type IDs
|
||||
- primary/display flag
|
||||
- active/inactive lifecycle
|
||||
|
||||
Effective CRM access is resolved from:
|
||||
|
||||
1. membership role
|
||||
2. all active CRM role assignments
|
||||
3. all assigned CRM role profile permissions
|
||||
4. direct membership permissions
|
||||
|
||||
Rules:
|
||||
|
||||
- permissions are the union of all active role-profile permissions plus membership permissions
|
||||
- branch scope is the union of active assignment scopes unless any active assignment grants `all`
|
||||
- product-type scope is the union of active assignment scopes unless any active assignment grants `all`
|
||||
- approval authority uses the highest active authority among assigned roles
|
||||
- primary role is display-only and does not limit the permission union
|
||||
- `memberships.businessRole` remains temporarily as a compatibility fallback only when no active CRM role assignment exists
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- supports realistic multi-role CRM operation
|
||||
- separates organization access from CRM-specific authorization
|
||||
- allows per-role scope assignment per user
|
||||
- keeps rollout compatible with existing membership data through lazy backfill
|
||||
|
||||
### Negative
|
||||
|
||||
- resolver complexity increases
|
||||
- old `memberships.businessRole` semantics must be maintained during transition
|
||||
- UI and audit coverage must handle assignment lifecycle, not just role-profile maintenance
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
- create `crm_user_role_assignments`
|
||||
- backfill one primary assignment from `memberships.businessRole` when possible
|
||||
- treat `memberships.businessRole` as deprecated for CRM authorization after Task L.1
|
||||
- keep the column until user-management and downstream integrations fully move to assignment-based CRM access
|
||||
78
docs/adr/0015-customer-ownership-contact-sharing.md
Normal file
78
docs/adr/0015-customer-ownership-contact-sharing.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# ADR 0015: Customer Ownership and Contact Sharing
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The CRM customer domain already supported customer masters, contacts, leads/enquiries, and quotations, but it still lacked two production behaviors:
|
||||
|
||||
- a first-class primary owner for each customer
|
||||
- a persistent sharing model for customer contacts
|
||||
|
||||
That gap caused two operational problems:
|
||||
|
||||
- Marketing could not reliably suggest the right sales owner when creating a lead.
|
||||
- Contact visibility depended on creator/admin fallbacks instead of explicit business governance.
|
||||
|
||||
## Decision
|
||||
|
||||
We introduce two persistent governance models.
|
||||
|
||||
### 1. Customer owner
|
||||
|
||||
`crm_customers` now stores:
|
||||
|
||||
- `owner_user_id`
|
||||
- `owner_assigned_at`
|
||||
- `owner_assigned_by`
|
||||
|
||||
Ownership changes are recorded in `crm_customer_owner_history`.
|
||||
|
||||
Rules:
|
||||
|
||||
- one primary owner at a time
|
||||
- owner is organization-scoped
|
||||
- owner changes are auditable
|
||||
- customer owner gains visibility to the customer and related CRM work, subject to CRM scope rules
|
||||
|
||||
### 2. Contact sharing
|
||||
|
||||
`crm_contact_shares` becomes the production source of truth for persistent contact sharing.
|
||||
|
||||
Rules:
|
||||
|
||||
- creator keeps access
|
||||
- explicitly shared users gain access
|
||||
- customer owner gains access
|
||||
- CRM admin and broader organization/team scopes keep access
|
||||
- removing a share revokes access by deactivating the share row
|
||||
|
||||
### 3. Lead assignment suggestion
|
||||
|
||||
Lead/enquiry creation now accepts an optional assignee suggestion.
|
||||
|
||||
When a selected customer has an owner:
|
||||
|
||||
- the form pre-selects that owner as the suggested sales owner
|
||||
- users may override before submit
|
||||
- the create route records whether the suggestion was used or overridden
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- customer responsibility is now explicit and historical
|
||||
- contact access is governed by durable business state instead of demo-only behavior
|
||||
- lead routing is more accurate at creation time
|
||||
|
||||
Tradeoffs:
|
||||
|
||||
- visibility logic across customer/contact/enquiry flows becomes more stateful
|
||||
- team-scope remains approximate until a first-class team graph exists
|
||||
|
||||
## Notes
|
||||
|
||||
- This ADR does not introduce multiple primary owners, temporary shares, or territory management.
|
||||
- Customer owner does not imply approval authority.
|
||||
58
docs/adr/0016-won-lost-lifecycle-governance.md
Normal file
58
docs/adr/0016-won-lost-lifecycle-governance.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# ADR 0016: Won / Lost Lifecycle Governance
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
CRM dashboard and reporting previously treated quotation acceptance and rejection as proxies for opportunity outcomes. That made business outcome data unstable because:
|
||||
|
||||
- quotation approval/acceptance does not equal PO received
|
||||
- lost opportunities could be inferred without a required lost reason
|
||||
- reopen rules were undefined
|
||||
- revenue and win/loss KPI logic depended on quotation status instead of the enquiry lifecycle
|
||||
|
||||
Business Discovery 3.0 froze the production rule set:
|
||||
|
||||
- `closed_won` means PO received
|
||||
- `closed_lost` requires a lost reason
|
||||
- won cannot reopen
|
||||
- lost can reopen with audit
|
||||
- won revenue prefers PO amount and falls back to quotation total
|
||||
- lost revenue uses quotation total
|
||||
|
||||
## Decision
|
||||
|
||||
We use `crm_enquiries` as the official business outcome record.
|
||||
|
||||
We add outcome fields on `crm_enquiries`:
|
||||
|
||||
- `closed_won_at`
|
||||
- `closed_lost_at`
|
||||
- `closed_by_user_id`
|
||||
- `po_number`
|
||||
- `po_date`
|
||||
- `po_amount`
|
||||
- `po_currency`
|
||||
- `lost_reason`
|
||||
- `lost_competitor`
|
||||
- `lost_remark`
|
||||
|
||||
We add `crm_enquiry_attachments` for purchase order files with category `purchase_order`.
|
||||
|
||||
All lifecycle transitions must go through the enquiry outcome service layer:
|
||||
|
||||
- `markEnquiryAsWon()`
|
||||
- `markEnquiryAsLost()`
|
||||
- `reopenLostEnquiry()`
|
||||
|
||||
Quotation status is no longer allowed to mutate enquiry outcome automatically.
|
||||
|
||||
## Consequences
|
||||
|
||||
- dashboard won/lost KPI must read enquiry `pipeline_stage`
|
||||
- marketing can see outcome state but PO amount remains server-side restricted
|
||||
- PO attachment access follows commercial-data visibility rules
|
||||
- audit coverage expands to `mark_won`, `mark_lost`, `reopen`, and `upload_po`
|
||||
- downstream reporting can reuse a stable outcome model for Task K
|
||||
48
docs/adr/0017-report-foundation.md
Normal file
48
docs/adr/0017-report-foundation.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# ADR 0017: CRM Report Foundation
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
CRM now has stable foundations for:
|
||||
|
||||
- access control and scope resolution
|
||||
- customer ownership and contact sharing
|
||||
- lead and enquiry lifecycle
|
||||
- won/lost governance
|
||||
- revenue attribution
|
||||
- dashboard KPI
|
||||
|
||||
Future report modules need one shared architecture so they do not re-implement:
|
||||
|
||||
- report discovery
|
||||
- filter contracts
|
||||
- export helpers
|
||||
- access resolution
|
||||
- audit logging
|
||||
|
||||
## Decision
|
||||
|
||||
We introduce a dedicated report foundation under `src/features/crm/reports/`.
|
||||
|
||||
The foundation includes:
|
||||
|
||||
- `crm_report_definitions` as the report registry
|
||||
- `crm_report_category` master options for grouping and future permissions
|
||||
- shared filter metadata endpoint
|
||||
- `ResolvedReportContext` for organization, scope, and permission resolution
|
||||
- `resolveCrmAccess()` as the common context adapter for reports
|
||||
- dataset and builder layers for report queries
|
||||
- shared CSV/XLS export helpers
|
||||
- `crm_report` audit entries for `view`, `export_csv`, and `export_excel`
|
||||
|
||||
Reports must not query the database directly from UI routes. Route handlers call the report server layer instead.
|
||||
|
||||
## Consequences
|
||||
|
||||
- K.2-K.7 can add business reports without redefining report plumbing
|
||||
- scope and permission enforcement stays aligned with CRM authorization work from Task L
|
||||
- export behavior becomes reusable instead of dashboard-specific
|
||||
- report navigation can be driven from the seeded registry and category metadata
|
||||
117
docs/adr/0018-lead-enquiry-domain-separation.md
Normal file
117
docs/adr/0018-lead-enquiry-domain-separation.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# ADR 0018: Lead / Enquiry Domain Separation
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0011 froze the current CRM lifecycle around a single persisted entity:
|
||||
|
||||
- `crm_enquiries` stores both marketing-owned leads and sales-owned enquiries
|
||||
- `pipeline_stage` acts as the discriminator
|
||||
- navigation separation exists, but domain separation does not
|
||||
|
||||
That model was an intentional stabilization step for Tasks D.3 and D.3.1. It reduced UX ambiguity without forcing a schema split during the first production rollout.
|
||||
|
||||
Task D.5 introduces a stronger business requirement:
|
||||
|
||||
- Marketing owns `Lead` as a dedicated entity
|
||||
- Sales owns `Enquiry` as a dedicated entity
|
||||
- one lead may create many enquiries
|
||||
- sales may also create enquiries directly without a lead
|
||||
- marketing outcome must be derived from sales execution, not manually controlled
|
||||
|
||||
The current single-record model now creates structural ambiguity in at least these areas:
|
||||
|
||||
1. Assignment currently mutates one record from `lead -> enquiry`, which destroys the original lead as a stable marketing record.
|
||||
2. A true `1 Lead -> N Enquiries` relationship cannot be represented cleanly while the lead itself is also the enquiry record.
|
||||
3. Dashboard and report logic still infer lead analytics from `crm_enquiries.pipeline_stage`, which couples marketing and sales datasets.
|
||||
4. Outcome synchronization for marketing is harder to reason about because the source marketing entity does not exist independently.
|
||||
|
||||
## Decision
|
||||
|
||||
We will replace the single-record lead/enquiry persistence model with a split domain model:
|
||||
|
||||
- `crm_leads` becomes the dedicated marketing-owned lead table
|
||||
- `crm_enquiries` remains the dedicated sales-owned enquiry table
|
||||
- `crm_enquiries.lead_id` becomes nullable to support:
|
||||
- lead-origin enquiries
|
||||
- direct sales enquiries with `lead_id = null`
|
||||
|
||||
### Lifecycle
|
||||
|
||||
The target business flow is:
|
||||
|
||||
```txt
|
||||
Lead
|
||||
-> Enquiry
|
||||
-> Quotation
|
||||
-> Won / Lost
|
||||
```
|
||||
|
||||
### Relationship rules
|
||||
|
||||
- `crm_leads 1 -> N crm_enquiries`
|
||||
- `crm_enquiries 1 -> N crm_quotations`
|
||||
- `crm_quotations` remains the owning record for quotation revisions and child resources
|
||||
|
||||
### Ownership rules
|
||||
|
||||
- marketing owns lead creation and lead follow-up
|
||||
- sales owns enquiry creation, enquiry follow-up, quotation generation, and won/lost execution
|
||||
- managers and admins retain cross-workspace monitoring based on resolved CRM access
|
||||
|
||||
### Outcome rules
|
||||
|
||||
- lead outcome is derived, never manually updated
|
||||
- when any linked enquiry becomes won, the lead outcome becomes `won`
|
||||
- when all linked enquiries are lost, the lead outcome becomes `lost`
|
||||
- otherwise the lead outcome remains `open`
|
||||
|
||||
### Sequence rules
|
||||
|
||||
- lead codes use a dedicated lead document sequence
|
||||
- enquiry codes keep a separate enquiry document sequence
|
||||
|
||||
### Reporting and dashboard rules
|
||||
|
||||
- lead KPI and lead reports read from `crm_leads`
|
||||
- enquiry KPI and enquiry reports read from `crm_enquiries`
|
||||
- no report should continue to treat `pipeline_stage` as the marketing/sales entity boundary after migration
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- aligns persistence with the real marketing-to-sales handoff
|
||||
- supports true `1 Lead -> N Enquiries`
|
||||
- preserves lead identity after assignment
|
||||
- simplifies marketing analytics and awareness-source reporting
|
||||
- makes direct sales enquiries first-class without overloading lead semantics
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- supersedes a previously accepted ADR and requires broad migration across services, reports, dashboard, and permissions
|
||||
- requires data migration from existing `crm_enquiries.pipeline_stage`
|
||||
- increases temporary compatibility complexity during rollout
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
The migration should be delivered in explicit phases:
|
||||
|
||||
1. Introduce ADR and governance freeze for split-domain direction.
|
||||
2. Add schema and seed support for `crm_leads`, `lead_id`, and lead master options.
|
||||
3. Introduce lead services and lead APIs without deleting enquiry flows.
|
||||
4. Migrate dashboard and reports to separated datasets.
|
||||
5. Remove old single-record assumptions from shared enquiry services and UI.
|
||||
|
||||
## Supersedes
|
||||
|
||||
This ADR is intended to supersede the following sections of ADR-0011 once implemented:
|
||||
|
||||
- `Single record model`
|
||||
- `Pipeline stage model` as the lead/enquiry persistence boundary
|
||||
- `Navigation Separation` as the primary separation mechanism
|
||||
|
||||
Until implementation is complete, ADR-0011 remains the description of current production behavior, while this ADR defines the approved target direction for Task D.5.
|
||||
8
docs/adr/ADR-0006-branch-domain-model.md
Normal file
8
docs/adr/ADR-0006-branch-domain-model.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Current:
|
||||
branch stored in ms_options
|
||||
|
||||
Future:
|
||||
organization_branches table
|
||||
|
||||
Reason:
|
||||
branch will contain business metadata and workflow configuration
|
||||
88
docs/audit/package-scripts-cleanup.md
Normal file
88
docs/audit/package-scripts-cleanup.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Package Scripts Cleanup Audit
|
||||
|
||||
## Scope
|
||||
|
||||
- reviewed `package.json`
|
||||
- reviewed `scripts/**`
|
||||
- reviewed `src/db/seeds/**`
|
||||
- reviewed `docs/operations/system-bootstrap.md`
|
||||
- cross-checked historical references in `docs/implementation/**` and setup copy
|
||||
|
||||
## Decision Summary
|
||||
|
||||
| Script Name | Current Command | Status | Reason | Replacement Command |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `dev` | `next dev` | keep | core runtime entrypoint | - |
|
||||
| `build` | `npm run verify:encoding && next build` | keep | build already guards encoding drift | - |
|
||||
| `start` | `next start` | keep | production runtime entrypoint | - |
|
||||
| `lint` | `oxlint` | keep | active lint entrypoint | - |
|
||||
| `lint:fix` | `oxlint --fix && bun format` | keep | active fix workflow | - |
|
||||
| `lint:strict` | `oxlint --deny-warnings` | keep | stricter CI-style lint path | - |
|
||||
| `format` | `oxfmt --write .` | keep | active formatter entrypoint | - |
|
||||
| `format:check` | `oxfmt --check .` | keep | active formatter verification | - |
|
||||
| `prepare` | `husky` | keep | install hook bootstrap | - |
|
||||
| `typecheck` | `tsc --noEmit` | add | required by task and docs verification flow | - |
|
||||
| `gen` | `npm run verify:encoding && npx drizzle-kit generate` | remove | duplicate of `db:generate` with unclear shorthand | `npm run db:generate` |
|
||||
| `db:generate` | `npx drizzle-kit generate` | keep | canonical drizzle generate command | - |
|
||||
| `db:migrate` | `npx drizzle-kit migrate` | keep | canonical drizzle migrate command | - |
|
||||
| `db:studio` | `npx drizzle-kit studio` | keep | canonical drizzle studio command | - |
|
||||
| `db:reset` | `node --experimental-strip-types scripts/db/reset-database.ts` | keep | guarded destructive operation with env safety | - |
|
||||
| `db:setup` | `npm run db:migrate && npm run seed:system` | keep | still referenced by setup flow and valid | - |
|
||||
| `db:setup:uat` | `npm run db:setup && npm run seed:uat` | keep | valid setup composition for UAT | - |
|
||||
| `db:fresh` | `npm run db:reset && npm run db:migrate && npm run seed:system` | keep | clean local bootstrap path | - |
|
||||
| `db:fresh:uat` | `npm run db:fresh && npm run seed:uat` | keep | clean UAT bootstrap path | - |
|
||||
| `migrate` | `npm run db:migrate` | remove | duplicate shorthand | `npm run db:migrate` |
|
||||
| `studio` | `npm run db:studio` | remove | duplicate shorthand | `npm run db:studio` |
|
||||
| `setup:db` | `npm run db:setup` | keep | compatibility alias still referenced by setup wizard copy | `npm run db:setup` |
|
||||
| `seed:super-admin` | `node scripts/seed-super-admin.js` | keep | useful low-level bootstrap step | - |
|
||||
| `seed:foundation` | `node --experimental-strip-types src/db/seeds/foundation.seed.ts` | keep | useful low-level seed for troubleshooting | - |
|
||||
| `seed:crm-uat` | `node --experimental-strip-types src/db/seeds/crm-uat.seed.ts` | keep | useful low-level data-only seed for troubleshooting | - |
|
||||
| `seed:system` | old direct seed file | replace | now routes through `scripts/seed-system.ts` wrapper and includes PDF template reseed | `npm run seed:system` |
|
||||
| `seed:uat` | old direct seed file | replace | now routes through `scripts/seed-uat.ts` wrapper and seeds users before CRM data | `npm run seed:uat` |
|
||||
| `seed:reset` | not present | add | task-required guarded reset + migrate + reseed orchestration | `npm run seed:reset -- --allow-db-reset` |
|
||||
| `seed:pdf-template` | `node --experimental-strip-types scripts/reseed-pdf-template-version.ts` | keep | canonical operator-facing PDF template sync | - |
|
||||
| `seed:pdf-template-version` | same as `seed:pdf-template` | keep | compatibility alias for historical docs and task lineage | `npm run seed:pdf-template` |
|
||||
| `pdf:reseed` | duplicate reseed alias | remove | overlapping alias with no added behavior | `npm run seed:pdf-template` |
|
||||
| `pdf:activate:product` | targeted activation command | keep | still useful for product-v1 operator flow | - |
|
||||
| `migrate:membership-business-roles` | migration utility | keep | one-off but still valid migration helper | - |
|
||||
| `seed:task-h1-fixture` | fixture utility | keep | still referenced by historical PDF verification docs | - |
|
||||
| `legacy:seed:task-h1-fixture` | duplicate legacy alias | remove | duplicate of active fixture command | `npm run seed:task-h1-fixture` |
|
||||
| `verify:task-h1` | fixture verification utility | keep | still referenced by historical docs | - |
|
||||
| `legacy:verify:task-h1` | duplicate legacy alias | remove | duplicate of active verification command | `npm run verify:task-h1` |
|
||||
| `verify:task-h3` | historical verification utility | keep | still valid and referenced | - |
|
||||
| `legacy:verify:task-h3` | duplicate legacy alias | remove | duplicate of active verification command | `npm run verify:task-h3` |
|
||||
| `verify:encoding` | `node scripts/verify-encoding.mjs` | keep | build safety prerequisite | - |
|
||||
| `verify:crm-access` | CRM access audit | keep | task-required verification | - |
|
||||
| `audit:pdf:*` | PDF audit suite | keep | active verification foundation | - |
|
||||
|
||||
## Changes Applied
|
||||
|
||||
- added `typecheck`
|
||||
- added `seed:reset`
|
||||
- moved `seed:system` to `scripts/seed-system.ts`
|
||||
- moved `seed:uat` to `scripts/seed-uat.ts`
|
||||
- kept `seed:pdf-template-version` only as compatibility alias
|
||||
- removed duplicate shorthand and legacy aliases:
|
||||
- `gen`
|
||||
- `migrate`
|
||||
- `studio`
|
||||
- `pdf:reseed`
|
||||
- `legacy:seed:task-h1-fixture`
|
||||
- `legacy:verify:task-h1`
|
||||
- `legacy:verify:task-h3`
|
||||
|
||||
## Seed Architecture Notes
|
||||
|
||||
- `seed:system` now guarantees:
|
||||
- super admin
|
||||
- foundation data
|
||||
- active PDF template reseed
|
||||
- `seed:uat` now guarantees:
|
||||
- deterministic UAT users and memberships
|
||||
- CRM role profiles and role assignments
|
||||
- CRM demo dataset
|
||||
- `seed:reset` now guarantees:
|
||||
- explicit `--allow-db-reset` confirmation or `ALLOW_DB_RESET=true`
|
||||
- database reset safety guard
|
||||
- migrations reapplied before seeding
|
||||
- `seed:system` then `seed:uat`
|
||||
31
docs/business/crm-terminology.md
Normal file
31
docs/business/crm-terminology.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# CRM Terminology Registry
|
||||
|
||||
| Code | English | Thai | Notes |
|
||||
| ---- | ------- | ---- | ----- |
|
||||
| crm | CRM | CRM | Keep as `CRM` |
|
||||
| dashboard | Dashboard | Dashboard | Keep as `Dashboard` |
|
||||
| lead | Lead | ลีด | ข้อมูลลูกค้าเป้าหมายก่อนส่งต่อให้ฝ่ายขาย |
|
||||
| enquiry | Enquiry | โอกาสขาย | โอกาสทางการขายที่มอบหมายให้ฝ่ายขายดำเนินการ |
|
||||
| quotation | Quotation | ใบเสนอราคา | ใช้กับทุกหน้าที่เกี่ยวกับเอกสารเสนอราคา |
|
||||
| approval | Approval | อนุมัติเอกสาร | ใช้กับ workflow และคิวอนุมัติ |
|
||||
| customer | Customer | ลูกค้า | ใช้กับทะเบียนลูกค้าและบทบาทลูกค้า |
|
||||
| contact | Contact | ผู้ติดต่อ | ใช้กับบุคคลติดต่อของลูกค้า |
|
||||
| billing_customer | Billing Customer | ลูกค้าผู้รับใบเสนอราคา | บริษัทที่ใช้สำหรับออกใบเสนอราคาและวางบิล |
|
||||
| project_parties | Project Parties | ผู้เกี่ยวข้องในโครงการ | ใช้กับ customer roles ใน enquiry และ quotation |
|
||||
| project_party.customer | Customer | ลูกค้า | บทบาทในโครงการ |
|
||||
| project_party.billing_customer | Billing Customer | ลูกค้าผู้รับใบเสนอราคา | บทบาทในโครงการ |
|
||||
| project_party.end_customer | End Customer | เจ้าของโครงการ | บทบาทในโครงการ |
|
||||
| project_party.contractor | Contractor | ผู้รับเหมา | บทบาทในโครงการ |
|
||||
| project_party.consultant | Consultant | ที่ปรึกษา | บทบาทในโครงการ |
|
||||
| lead_count | Lead Count | จำนวนลีด | KPI dashboard |
|
||||
| enquiry_count | Enquiry Count | จำนวนโอกาสขาย | KPI dashboard |
|
||||
| won_count | Won Count | จำนวนงานที่ชนะ | KPI dashboard |
|
||||
| lost_count | Lost Count | จำนวนงานที่แพ้ | KPI dashboard |
|
||||
| revenue | Revenue | มูลค่าใบเสนอราคา | KPI / report label |
|
||||
| conversion_rate | Conversion Rate | อัตราการเปลี่ยนสถานะ | KPI / report label |
|
||||
|
||||
## Freeze Policy
|
||||
|
||||
- เอกสารนี้เป็น source of truth ของคำธุรกิจ CRM ฝั่ง UI
|
||||
- การเปลี่ยนคำหลังจากนี้ต้องผ่าน ADR หรือ business review ก่อน
|
||||
- ห้ามใช้เอกสารนี้เพื่อเปลี่ยนชื่อ schema, API route, TypeScript type, หรือ service folder
|
||||
50
docs/business/pdf-mapping-registry.md
Normal file
50
docs/business/pdf-mapping-registry.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# PDF Mapping Registry
|
||||
|
||||
This document is the source of truth for active quotation PDF field ownership.
|
||||
|
||||
| PDF Field | Source Path | Transform | Example Value |
|
||||
| --- | --- | --- | --- |
|
||||
| `customer_name` | `customer.name` | direct | `ALLA Demo Customer` |
|
||||
| `customer_addr` | `customer.address` | direct multiline | `99/9 Rama IX Road, Bangkok 10310` |
|
||||
| `customer_tel` | `customer.phone` | direct | `02-000-1000` |
|
||||
| `customer_email` | `customer.email` | direct | `demo-customer@local.test` |
|
||||
| `customer_att` | `quotation.attention` | direct | `Task H.1 Contact` |
|
||||
| `project_name` | `quotation.projectName` | direct | `Task H.5 Audit Fixture` |
|
||||
| `site_location` | `quotation.projectLocation` | direct multiline | `Bangkok HQ` |
|
||||
| `tel_label` | `pdfme.labels.tel` | static label | `Tel` |
|
||||
| `email_label` | `pdfme.labels.email` | static label | `Email` |
|
||||
| `att_label` | `pdfme.labels.att` | static label | `Att` |
|
||||
| `project_label` | `pdfme.labels.project` | static label | `Project` |
|
||||
| `site_label` | `pdfme.labels.location` | static label | `Location` |
|
||||
| `quotation_date` | `pdfme.quotation_date` | `formatPdfDate()` canonical alias | `16 Jun 2026` |
|
||||
| `quotation_date_data` | `pdfme.quotation_date` | active-template field-name mapping | `16 Jun 2026` |
|
||||
| `quotation_code` | `quotation.code` | direct canonical alias | `QT-H5-AUDIT` |
|
||||
| `quotation_code_data` | `quotation.code` | active-template field-name mapping | `QT-H5-AUDIT` |
|
||||
| `quotation_price_data` | `pdfme.quotation_price_data` | `buildPdfPriceTableData()` | `[["Price (Exclude VAT)", "฿125,000.00", " ", "THB"]]` |
|
||||
| `quotation_price` | `pdfme.quotation_price` | compatibility scalar | `THB 125,000.00` |
|
||||
| `currency` | `quotation.currency` | compatibility scalar | `THB` |
|
||||
| `app1` | `signatures.preparedBy.name` | direct | `Task H.1 Admin` |
|
||||
| `app1_position` | `signatures.preparedBy.position` | position resolver | `Sales Manager` |
|
||||
| `app2` | `signatures.approvedBy.name` | direct | `Task H.1 Admin` |
|
||||
| `app2_position` | `signatures.approvedBy.position` | position resolver | `Sales Manager` |
|
||||
| `app3` | `signatures.authorizedBy.name` | direct | `Task H.1 Admin` |
|
||||
| `app3_position` | `signatures.authorizedBy.position` | position resolver | `Sales Manager` |
|
||||
| `topic` | dynamic topic engine | runtime schema clone | topic label table |
|
||||
| `data_topic` | dynamic topic engine | runtime schema clone | topic item table |
|
||||
| `topic_*` | `documentData.topics.all[*].topicType` | runtime generated | `Scope` |
|
||||
| `item_topic_*` | `documentData.topics.all[*].items` | `formatTopicItems()` | topic rows |
|
||||
|
||||
## Version Remap Rules
|
||||
|
||||
- template JSON changes must create a new `crm_document_template_versions` row instead of patching the old version in place
|
||||
- active mappings are rebuilt only for fields or placeholder tokens that still exist in the new version
|
||||
- previous approved snapshots may keep referencing retained inactive versions
|
||||
- deleted fields from the previous version, such as `exclusion_data` and `exclusion_label`, must not remain active mappings on the new version
|
||||
|
||||
## Alias Notes
|
||||
|
||||
- current PDFME schema uses field containers named `quotation_date_data` and `quotation_code_data`
|
||||
- runtime normalization publishes both canonical aliases (`quotation_date`, `quotation_code`) and the active field-name mappings so preview, audit, and render stay aligned
|
||||
- `quotation_price_data` is the canonical price table contract
|
||||
- `quotation_price` and `currency` remain compatibility scalars because the active table content still embeds those placeholder tokens
|
||||
- static labels are runtime-driven through `documentData.pdfme.labels.*`, not designer-only hardcoded text
|
||||
47
docs/business/pdf-placeholder-registry.md
Normal file
47
docs/business/pdf-placeholder-registry.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# PDF Placeholder Registry
|
||||
|
||||
This registry freezes the placeholder naming contract for quotation PDF templates.
|
||||
|
||||
| Placeholder | Type | Ownership | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `customer_name` | mapped | DB mapping | customer display name |
|
||||
| `customer_addr` | mapped | DB mapping | multiline address |
|
||||
| `customer_tel` | mapped | DB mapping | customer phone |
|
||||
| `customer_email` | mapped | DB mapping | customer email |
|
||||
| `customer_att` | mapped | DB mapping | quotation attention |
|
||||
| `project_name` | mapped | DB mapping | project name |
|
||||
| `site_location` | mapped | DB mapping | project location |
|
||||
| `tel_label` | mapped | DB mapping | static label from `pdfme.labels.tel` |
|
||||
| `email_label` | mapped | DB mapping | static label from `pdfme.labels.email` |
|
||||
| `att_label` | mapped | DB mapping | static label from `pdfme.labels.att` |
|
||||
| `project_label` | mapped | DB mapping | static label from `pdfme.labels.project` |
|
||||
| `site_label` | mapped | DB mapping | static label from `pdfme.labels.location` |
|
||||
| `quotation_date` | mapped | token alias | canonical formatted date token |
|
||||
| `quotation_date_data` | mapped | DB mapping | active field-name mapping for date |
|
||||
| `quotation_code` | mapped | token alias | canonical quotation code token |
|
||||
| `quotation_code_data` | mapped | DB mapping | active field-name mapping for code |
|
||||
| `quotation_price_data` | mapped | DB mapping | canonical price table `string[][]` |
|
||||
| `quotation_price` | mapped | DB mapping | compatibility scalar token inside price table |
|
||||
| `currency` | mapped | DB mapping | compatibility scalar token inside price table |
|
||||
| `app1` | mapped | DB mapping | prepared by name |
|
||||
| `app1_position` | mapped | DB mapping | prepared by position |
|
||||
| `app2` | mapped | DB mapping | approved by name |
|
||||
| `app2_position` | mapped | DB mapping | approved by position |
|
||||
| `app3` | mapped | DB mapping | authorized by name |
|
||||
| `app3_position` | mapped | DB mapping | authorized by position |
|
||||
| `topic` | reserved | dynamic topic engine | base runtime topic label template |
|
||||
| `data_topic` | reserved | dynamic topic engine | base runtime topic rows template |
|
||||
| `item_topic` | legacy reserved token | dynamic topic engine | allowed in schema token inventory, never stored as DB mapping row |
|
||||
| `topic_*` | generated | dynamic topic engine | runtime-only generated keys |
|
||||
| `item_topic_*` | generated | dynamic topic engine | runtime-only generated keys |
|
||||
| `quotation_date_labe` | static visual | designer-owned | tolerated typo-like visual label |
|
||||
| `quotation_code_lable` | static visual | designer-owned | tolerated typo-like visual label |
|
||||
| `line` | static visual | designer-owned | readonly line artifact, no mapping |
|
||||
|
||||
## Naming Rules
|
||||
|
||||
- do not store `topic_*` or `item_topic_*` keys in `crm_document_template_mappings`
|
||||
- keep `topic` and `data_topic` reserved for runtime schema cloning
|
||||
- treat typo-like names such as `quotation_date_labe` and `quotation_code_lable` as static visual fields, not business placeholders
|
||||
- when the designer removes a field from the next template version, the remap script must not carry that mapping forward
|
||||
- approved snapshots remain immutable to the template version they were generated from, even after that version becomes inactive
|
||||
79
docs/implementation/hotfix-1-customer-sub-group.md
Normal file
79
docs/implementation/hotfix-1-customer-sub-group.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Hotfix 1: Customer Sub Group
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `drizzle/0011_brief_hobgoblin.sql`
|
||||
- `docs/implementation/hotfix-1-customer-sub-group.md`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/db/schema.ts`
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `src/features/crm/customers/api/types.ts`
|
||||
- `src/features/crm/customers/schemas/customer.schema.ts`
|
||||
- `src/features/crm/customers/server/service.ts`
|
||||
- `src/app/api/crm/customers/route.ts`
|
||||
- `src/app/api/crm/customers/[id]/route.ts`
|
||||
- `src/components/forms/fields/select-field.tsx`
|
||||
- `src/features/crm/customers/components/customer-form-sheet.tsx`
|
||||
- `src/features/crm/customers/components/customer-columns.tsx`
|
||||
- `src/features/crm/customers/components/customer-detail.tsx`
|
||||
|
||||
## 3. Schema Field Used/Added
|
||||
|
||||
- Added optional field `customerSubGroup` to `crm_customers`
|
||||
- Field is stored with the same option-id pattern already used by `customerGroup`
|
||||
|
||||
## 4. Master Options Seeded
|
||||
|
||||
- `crm_customer_group`
|
||||
- `industrial`
|
||||
- `construction`
|
||||
- `government`
|
||||
- `dealer`
|
||||
- `other`
|
||||
- `crm_customer_sub_group`
|
||||
- `factory`
|
||||
- `warehouse`
|
||||
- `logistics`
|
||||
- `contractor`
|
||||
- `consultant`
|
||||
- `state_enterprise`
|
||||
- `government_agency`
|
||||
- `reseller`
|
||||
- `other`
|
||||
|
||||
Child options resolve and store `parentId` from the seeded `crm_customer_group` rows during idempotent seed execution.
|
||||
|
||||
## 5. Form Behavior Added
|
||||
|
||||
- Add/Edit Customer now includes `Customer Sub Group`
|
||||
- Sub group is disabled until a customer group is selected
|
||||
- Edit mode loads the saved group and sub group
|
||||
- Submit payload now persists `customerSubGroup`
|
||||
|
||||
## 6. Parent/Child Filtering Behavior
|
||||
|
||||
- Customer sub group options are filtered by `parentId === selectedCustomerGroup`
|
||||
- Changing group clears the sub group when the selected child no longer belongs to the new parent
|
||||
- Placeholder shows `Select customer group first` until a parent group is chosen
|
||||
|
||||
## 7. Validation Added
|
||||
|
||||
- API schemas now accept `customerSubGroup`
|
||||
- Server validation checks that:
|
||||
- the selected sub group exists in `crm_customer_sub_group`
|
||||
- a sub group cannot be submitted without a group
|
||||
- the selected sub group belongs to the selected group parent
|
||||
- Invalid parent/child combinations return a user-safe `400` error
|
||||
|
||||
## 8. Verification Result
|
||||
|
||||
- `npx tsc --noEmit` passed
|
||||
- CRUD wiring, list/detail output, and form filtering compile successfully
|
||||
|
||||
## 9. Remaining Risks
|
||||
|
||||
- Database migration `drizzle/0011_brief_hobgoblin.sql` still needs to be applied in the target environment
|
||||
- Seed changes were updated in code, but I did not run a live seed against the database in this session
|
||||
- Existing customer rows will keep `customerSubGroup = null` until edited or backfilled
|
||||
273
docs/implementation/pdf-audit-report.json
Normal file
273
docs/implementation/pdf-audit-report.json
Normal file
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"generatedAt": "2026-06-30T16:18:57.428Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "CRA2606-1108",
|
||||
"expectedFixtureCode": "QT-H5-AUDIT",
|
||||
"template": {
|
||||
"templateName": "ALLA Demo Quotation Standard",
|
||||
"version": "1.0",
|
||||
"versionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84",
|
||||
"fieldCount": 44,
|
||||
"previousVersion": null,
|
||||
"deletedFields": [],
|
||||
"newFields": [
|
||||
"Please_do_not",
|
||||
"app1",
|
||||
"app1_position",
|
||||
"app2",
|
||||
"app2_position",
|
||||
"app3",
|
||||
"app3_position",
|
||||
"att_label",
|
||||
"colon",
|
||||
"colon copy",
|
||||
"colon_att",
|
||||
"colon_email",
|
||||
"colon_fax",
|
||||
"colon_project",
|
||||
"colon_site",
|
||||
"colon_tel",
|
||||
"company1",
|
||||
"company2",
|
||||
"company_addr1",
|
||||
"company_addr2",
|
||||
"company_email",
|
||||
"company_tax",
|
||||
"company_tel",
|
||||
"customer_addr",
|
||||
"customer_att",
|
||||
"customer_email",
|
||||
"customer_name",
|
||||
"customer_tel",
|
||||
"data_topic",
|
||||
"dear_sirs",
|
||||
"email_label",
|
||||
"line",
|
||||
"project_label",
|
||||
"project_name",
|
||||
"quotation_code_data",
|
||||
"quotation_code_lable",
|
||||
"quotation_date_data",
|
||||
"quotation_date_labe",
|
||||
"quotation_price_data",
|
||||
"site_label",
|
||||
"site_location",
|
||||
"tel_label",
|
||||
"topic",
|
||||
"yours_faithfuly"
|
||||
],
|
||||
"duplicateFields": [],
|
||||
"unknownFields": [],
|
||||
"classifications": [
|
||||
{
|
||||
"name": "company1",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company2",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company_addr1",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company_addr2",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company_tel",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company_email",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "company_tax",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "line",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "customer_name",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "customer_addr",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "customer_tel",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "customer_email",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "dear_sirs",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "quotation_price_data",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "quotation_date_labe",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "quotation_date_data",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "quotation_code_lable",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "quotation_code_data",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "customer_att",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "project_name",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "site_location",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "colon",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon copy",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_tel",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_fax",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_email",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_att",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_project",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "colon_site",
|
||||
"classification": "readonly/static visual field"
|
||||
},
|
||||
{
|
||||
"name": "tel_label",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "email_label",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "att_label",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "project_label",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "site_label",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "topic",
|
||||
"classification": "dynamic topic template"
|
||||
},
|
||||
{
|
||||
"name": "data_topic",
|
||||
"classification": "dynamic topic template"
|
||||
},
|
||||
{
|
||||
"name": "Please_do_not",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "yours_faithfuly",
|
||||
"classification": "system field"
|
||||
},
|
||||
{
|
||||
"name": "app1",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "app1_position",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "app2",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "app2_position",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "app3",
|
||||
"classification": "static input field"
|
||||
},
|
||||
{
|
||||
"name": "app3_position",
|
||||
"classification": "static input field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"topicCount": 3,
|
||||
"itemCount": 4,
|
||||
"partyCount": 1,
|
||||
"dynamicTopicFieldCount": 3,
|
||||
"dynamicTopicItemFieldCount": 3,
|
||||
"mappingCount": 23
|
||||
},
|
||||
"mappingCoverage": {
|
||||
"placeholderTokenCount": 12,
|
||||
"unmappedTokens": [],
|
||||
"orphanMappings": [],
|
||||
"priceTableMapping": {
|
||||
"sourcePath": "pdfme.quotation_price_data",
|
||||
"dataType": "table"
|
||||
},
|
||||
"itemTableColumns": []
|
||||
},
|
||||
"runtimePayload": {
|
||||
"templateInputKeys": 31,
|
||||
"topicInputKeys": 6,
|
||||
"priceTableRows": 1
|
||||
},
|
||||
"consistency": {
|
||||
"status": "PASS",
|
||||
"approvedSnapshotAvailable": true,
|
||||
"approvedArtifactReference": null,
|
||||
"inconsistentKeys": []
|
||||
},
|
||||
"integritySummary": {
|
||||
"available": true,
|
||||
"artifactDir": "C:\\Users\\mtpphtaps\\Documents\\gitea\\alla-allaos-fullstack\\artifacts\\pdf-audit"
|
||||
}
|
||||
}
|
||||
31
docs/implementation/pdf-audit-report.md
Normal file
31
docs/implementation/pdf-audit-report.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# PDF Audit Report
|
||||
|
||||
- Generated At: 2026-06-30T16:18:57.428Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: CRA2606-1108
|
||||
- Template: ALLA Demo Quotation Standard v1.0
|
||||
- Previous Version: None
|
||||
|
||||
## Metrics
|
||||
|
||||
- Topic Count: 3
|
||||
- Item Count: 4
|
||||
- Project Party Count: 1
|
||||
- Mapping Count: 23
|
||||
|
||||
## Mapping Coverage
|
||||
|
||||
- Placeholder Tokens: 12
|
||||
- Unmapped Tokens: None
|
||||
- Orphan Mappings: None
|
||||
|
||||
## Visual / Consistency
|
||||
|
||||
- Approved Snapshot Available: Yes
|
||||
- Consistency Status: PASS
|
||||
- Inconsistent Keys: None
|
||||
|
||||
## Integrity Layer
|
||||
|
||||
- Integrity Summary Available: Yes
|
||||
- Artifact Directory: C:\Users\mtpphtaps\Documents\gitea\alla-allaos-fullstack\artifacts\pdf-audit
|
||||
37
docs/implementation/pdf-parity-checklist.md
Normal file
37
docs/implementation/pdf-parity-checklist.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# PDF Parity Checklist
|
||||
|
||||
## Header
|
||||
|
||||
| Area | Expectation | Status |
|
||||
| --- | --- | --- |
|
||||
| Company block | Static company branding and contact text render from template | Partial |
|
||||
| Customer block | `customer_name`, `customer_addr`, `customer_tel`, `customer_email`, `customer_att` render from mapping registry | Match |
|
||||
| Document references | `quotation_code`, `quotation_date`, `project_name`, `site_location` render from live quotation data | Match |
|
||||
|
||||
## Commercial Body
|
||||
|
||||
| Area | Expectation | Status |
|
||||
| --- | --- | --- |
|
||||
| Items | `items_table` columns stay aligned with quotation item rows and totals | Match |
|
||||
| Topics | Dynamic topic engine expands `topic_*` / `item_topic_*` in order | Match |
|
||||
| Exclusion block | `exclusion_data` renders as `string[][]` table payload | Match |
|
||||
| Price summary | `quotation_price` and `currency` use formatted runtime values | Match |
|
||||
|
||||
## Approval / Footer
|
||||
|
||||
| Area | Expectation | Status |
|
||||
| --- | --- | --- |
|
||||
| Signatures | `app1/app2/app3` map to prepared/approved/authorized blocks | Match |
|
||||
| Signature placement | Closing block stays together after dynamic topic pagination | Partial |
|
||||
| Footer copy | Static footer text and closing statement remain template-owned | Match |
|
||||
|
||||
## Visual Parity Rules
|
||||
|
||||
- `Match` means the current PDF runtime has a verified mapping path and auditable payload.
|
||||
- `Partial` means the business data path is correct but final pixel-perfect comparison still needs manual screenshot review against the legacy CRM PDF.
|
||||
- `Missing` should only be used when a field or section has no runtime path.
|
||||
|
||||
## Integrity Freeze
|
||||
|
||||
- `npm run audit:pdf` is the integrity gate for template, placeholder, payload, topic runtime, and snapshot parity.
|
||||
- `QT-H5-AUDIT` is the preferred fixture for repeatable PDF audits.
|
||||
538
docs/implementation/pdfme-legacy-audit.md
Normal file
538
docs/implementation/pdfme-legacy-audit.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# PDFME Legacy Audit
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
This audit compares the legacy PDFME quotation generation implementation with the current ALLA OS CRM vNext implementation. The investigation reveals significant gaps in placeholder mapping, data transformation, and topic handling that explain why the current system displays incomplete or incorrectly mapped data.
|
||||
|
||||
**Key Finding**: The legacy implementation uses a direct mapping approach with specialized transform functions for each field, while the current implementation relies on a generic mapping system that may not handle all edge cases correctly.
|
||||
|
||||
---
|
||||
|
||||
## 2. Legacy Files Inspected
|
||||
|
||||
### 2.1 Core Files
|
||||
- **`src/features/quotations/lib/pdfme-export.ts`** - Main PDFME generation logic
|
||||
- **`src/features/quotations/lib/quotation-preview-utils.ts`** - Format utilities
|
||||
- **`src/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json`** - Template definition
|
||||
- **`src/features/quotations/types/quotation-preview.types.ts`** - Type definitions
|
||||
|
||||
### 2.2 Current Project Files
|
||||
- **`src/features/foundation/pdf-generator/server/service.ts`** - PDF generation service
|
||||
- **`src/features/crm/quotations/document/server/service.ts`** - Document data builder
|
||||
- **`src/features/foundation/document-template/server/service.ts`** - Template mapping service
|
||||
- **`src/db/schema.ts`** - Database schema
|
||||
|
||||
---
|
||||
|
||||
## 3. Legacy PDFME Flow
|
||||
|
||||
### 3.1 Architecture
|
||||
```
|
||||
generateQuotationPDF()
|
||||
↓
|
||||
mapDataToTemplate()
|
||||
↓
|
||||
formatDate() / formatCurrency()
|
||||
↓
|
||||
formatTopicItems() / section() / section2()
|
||||
↓
|
||||
generateExtraTopicFields()
|
||||
↓
|
||||
generate() from @pdfme/generator
|
||||
```
|
||||
|
||||
### 3.2 Key Characteristics
|
||||
- **Direct mapping**: Each placeholder key is explicitly mapped in `mapDataToTemplate()`
|
||||
- **Inline transforms**: Transform functions are applied inline during mapping
|
||||
- **Dynamic schema generation**: Extra topics modify template schema at runtime
|
||||
- **Multi-branch support**: Different templates for `alla` vs `onvalla`
|
||||
- **Specialized formatting**: Currency, date, and topic item formatting
|
||||
|
||||
### 3.3 Data Structure
|
||||
```typescript
|
||||
{
|
||||
customer: QuotationPreviewCustomer
|
||||
quotation: QuotationPreviewData
|
||||
items: QuotationPreviewItem[]
|
||||
topics: Array<{ topicType: string; items: Array<{ content: string }> }>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Legacy Placeholder Inventory
|
||||
|
||||
| Placeholder | Legacy Source | Transform | Current Mapping | Status |
|
||||
|-------------|----------------|-----------|------------------|--------|
|
||||
| `customer_name` | `customer.name` | Fallback to `'-'` | ✅ Mapped | OK |
|
||||
| `customer_addr` | `customer.address` | Fallback to `'-'` | ✅ Mapped | OK |
|
||||
| `customer_tel` | `customer.contacts[primary].mobile/phone` | Prefix: `Tel : ` | ✅ Mapped | OK |
|
||||
| `customer_email` | `customer.contacts[primary].email` | Prefix: `Email : ` | ✅ Mapped | OK |
|
||||
| `customer_att` | `quotation.attention` | Direct | ✅ Mapped | OK |
|
||||
| `project_name` | `quotation.projectName` | Direct | ✅ Mapped | OK |
|
||||
| `site_location` | `quotation.projectLocation` | Direct | ✅ Mapped | OK |
|
||||
| `quotation_date` | `quotation.quotationDate` | `formatDate(date, 'en')` | ✅ Mapped | ⚠️ **Check format** |
|
||||
| `quotation_code` | `quotation.code` | Direct | ✅ Mapped | OK |
|
||||
| `quotation_price` | `quotation.totalAmount` | `formatCurrency(amount, currency)` | ✅ Mapped | ⚠️ **Check format** |
|
||||
| `currency` | `quotation.currency` | Direct | ✅ Mapped | OK |
|
||||
| `exclusion_label` | `termsTopic.topicType` | Hardcoded: `"Exclusion from scope of Supply:"` | ❌ **Missing** | **MISSING** |
|
||||
| `exclusion_data` | `termsTopic.items` | `formatTopicItems()` → `string[][]` | ❌ **Missing** | **MISSING** |
|
||||
| `Terms_of_payment` | `termsTopic.topicType` | Hardcoded: `"Terms of payment upon progress of each item:"` | ❌ **Missing** | **MISSING** |
|
||||
| `data_term_of_payment` | `termsTopic.items` | `formatTopicItems()` → `string[][]` | ❌ **Missing** | **MISSING** |
|
||||
| `warranty` | `warrantyTopic.topicType` | Hardcoded: `"Warranty:"` | ❌ **Missing** | **MISSING** |
|
||||
| `data_warranty` | `warrantyTopic.items` | `formatTopicItems()` → `string[][]` | ❌ **Missing** | **MISSING** |
|
||||
| `Delivery` | `deliveryTopic.topicType` | Hardcoded: `"Delivery:"` | ❌ **Missing** | **MISSING** |
|
||||
| `data_delivery` | `deliveryTopic.items` | `formatTopicItems()` → `string[][]` | ❌ **Missing** | **MISSING** |
|
||||
| `Affter service` | `deliveryTopic.topicType` | Hardcoded: `"Affter service:"` | ❌ **Missing** | **MISSING** |
|
||||
| `data_affter_service` | `deliveryTopic.items` | `formatTopicItems()` → `string[][]` | ❌ **Missing** | **MISSING** |
|
||||
| `topic` | **Static text** | Hardcoded: `"Topic:"` | ✅ Static | OK |
|
||||
| `data_topic` | `topics` | **Complex transform needed** | ❌ **Missing** | **MISSING** |
|
||||
| `item_topic` | **Not in legacy** | - | ❌ **Not defined** | **TEMPLATE MISMATCH** |
|
||||
| `app1` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
| `app1_position` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
| `app2` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
| `app2_position` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
| `app3` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
| `app3_position` | **Not populated** | Static: `"Type Something..."` | ❌ **Missing** | **MISSING** |
|
||||
|
||||
---
|
||||
|
||||
## 5. Legacy Transform Functions
|
||||
|
||||
### 5.1 `formatDate(dateString: string, language: 'th' | 'en'): string`
|
||||
**Purpose**: Format date for PDF display
|
||||
**Input**: ISO date string
|
||||
**Output**: Localized date string (Thai: Buddhist calendar, English: Gregorian)
|
||||
**Why it matters**: PDF requires specific date format
|
||||
**Current project**: Has equivalent but may differ in format
|
||||
|
||||
```typescript
|
||||
// Legacy
|
||||
export function formatDate(dateString: string, language: 'th' | 'en'): string {
|
||||
const date = new Date(dateString);
|
||||
if (language === 'th') {
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
calendar: 'buddhist'
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 `formatCurrency(amount: number, currencyCode: CurrencyCode): string`
|
||||
**Purpose**: Format currency with Thai locale
|
||||
**Input**: Amount and currency code
|
||||
**Output**: Formatted string (e.g., "฿1,000,000.00")
|
||||
**Why it matters**: PDF price display requires proper formatting
|
||||
**Current project**: May have equivalent but needs verification
|
||||
|
||||
```typescript
|
||||
// Legacy
|
||||
export function formatCurrency(amount: number, currencyCode: CurrencyCode): string {
|
||||
const symbol = getCurrencySymbol(currencyCode);
|
||||
const formatted = new Intl.NumberFormat('th-TH', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 `formatTopicItems(topic: any): string[][]`
|
||||
**Purpose**: Transform topic items to PDFME table format
|
||||
**Input**: Topic object with `items` array
|
||||
**Output**: 2D array where each item is wrapped in array
|
||||
**Why it matters**: PDFME tables require `string[][]` format
|
||||
**Current project**: ❌ **MISSING**
|
||||
|
||||
```typescript
|
||||
// Legacy
|
||||
function formatTopicItems(topic: any): string[][] {
|
||||
if (!topic || !topic.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item: any) => [item.content || '-']);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 `section(labelKey: string, valueKey: string, topic: any, label: string)`
|
||||
**Purpose**: Create label and data table pairs
|
||||
**Input**: Label key, value key, topic data, label text
|
||||
**Output**: Object with two keys mapping to `string[][]`
|
||||
**Why it matters**: Handles topic sections with consistent label/data pattern
|
||||
**Current project**: ❌ **MISSING**
|
||||
|
||||
```typescript
|
||||
// Legacy
|
||||
function section(labelKey: string, valueKey: string, topic: any, label: string) {
|
||||
if (!topic) return {};
|
||||
return {
|
||||
[labelKey]: [[label]],
|
||||
[valueKey]: formatTopicItems(topic)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 `section2(labelKey: string, valueKey: string, topic: any, label: string)`
|
||||
**Purpose**: Handle extra topics (unmapped/dynamic topics)
|
||||
**Input**: Same as `section` but supports arrays
|
||||
**Output**: Object with numbered keys for multiple topics
|
||||
**Why it matters**: Allows unlimited extra topics with dynamic numbering
|
||||
**Current project**: ❌ **MISSING**
|
||||
|
||||
### 5.6 `generateExtraTopicFields(extraTopics: any[]): any[]`
|
||||
**Purpose**: Dynamically generate PDFME schema fields for extra topics
|
||||
**Input**: Array of extra topics
|
||||
**Output**: Array of PDFME field definitions
|
||||
**Why it matters**: Allows extra topics to appear in PDF without template modification
|
||||
**Current project**: ❌ **MISSING**
|
||||
|
||||
---
|
||||
|
||||
## 6. Legacy Table Data Format
|
||||
|
||||
### 6.1 Topic Tables (Scope, Exclusion, Payment, Warranty, Delivery)
|
||||
**Expected Format**: `string[][]`
|
||||
- Each row is an array of strings
|
||||
- Each item becomes: `[item.content]`
|
||||
- Empty topic becomes: `[["-"]]`
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Input
|
||||
{
|
||||
topicType: "scope",
|
||||
items: [
|
||||
{ content: "Provide crane" },
|
||||
{ content: "Install at site" }
|
||||
]
|
||||
}
|
||||
|
||||
// Output
|
||||
[
|
||||
["Provide crane"],
|
||||
["Install at site"]
|
||||
]
|
||||
```
|
||||
|
||||
### 6.2 Price Table
|
||||
**Format**: `[[string, string, string, string]]`
|
||||
- Hardcoded 4 columns
|
||||
- Example: `[[\"Price (Exclude VAT)\",\"฿1,000,000.00\",\" \",\"THB\"]]`
|
||||
|
||||
### 6.3 Current Project Table Handling
|
||||
**Issue**: Current `mapDocumentDataToTemplateInput()` returns table data as:
|
||||
- Array of objects for `dataType: 'table'`
|
||||
- May not format as `string[][]` correctly
|
||||
- May not handle single-column tables properly
|
||||
|
||||
---
|
||||
|
||||
## 7. Legacy Topic Handling
|
||||
|
||||
### 7.1 Product Type Mapping
|
||||
Legacy maps quotation type to topic type keys:
|
||||
|
||||
```typescript
|
||||
const TOPIC_MAPPING = {
|
||||
crane: {
|
||||
scope: 'scope_of_work',
|
||||
exclusion: 'exclusion_from_scope_of_supply',
|
||||
terms: 'terms_of_payment_upon_progress_of_each_item',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
dockdoor: {
|
||||
scope: 'dock_scope_of_work',
|
||||
exclusion: 'exclusion_of_supply_installation',
|
||||
terms: 'payment_conditions',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
solarcell: {
|
||||
scope: 'solarcell_scope_of_work',
|
||||
exclusion: 'solarcell_exclusion_of_supply_installation',
|
||||
terms: 'solarcell_payment_conditions',
|
||||
delivery: 'solarcell_delivery',
|
||||
warranty: 'solarcell_warranty'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Topic Splitting Logic
|
||||
Legacy splits topics into two categories:
|
||||
1. **Mapped topics**: scope, exclusion, terms, delivery, warranty (based on quotation type)
|
||||
2. **Extra topics**: All other topics (dynamic)
|
||||
|
||||
### 7.3 Topic Processing
|
||||
- Mapped topics use `section()` helper
|
||||
- Extra topics use `section2()` helper with numbering
|
||||
- Extra topics generate dynamic PDFME schema fields
|
||||
|
||||
### 7.4 Current Project Topic Handling
|
||||
**Issues**:
|
||||
- No product type mapping
|
||||
- No topic splitting
|
||||
- No dynamic schema generation
|
||||
- Topics likely treated uniformly, losing type-specific labels
|
||||
|
||||
---
|
||||
|
||||
## 8. Legacy Approval / Signature Handling
|
||||
|
||||
### 8.1 Approval Fields in Template
|
||||
Template has static signature fields:
|
||||
- `app1`, `app1_position`
|
||||
- `app2`, `app2_position`
|
||||
- `app3`, `app3_position`
|
||||
|
||||
### 8.2 Legacy Population
|
||||
**Status**: Not populated in legacy code
|
||||
- Fields contain static placeholder: `"Type Something..."`
|
||||
- Appears to be manually filled or signature-only
|
||||
- No dynamic data source
|
||||
|
||||
### 8.3 Current Project
|
||||
**Status**: ❌ **NOT MAPPED**
|
||||
- No mapping for approval fields
|
||||
- No source data for signatures
|
||||
- Approval data exists but not connected to PDF
|
||||
|
||||
---
|
||||
|
||||
## 9. Current Project Gaps
|
||||
|
||||
### 9.1 Critical Missing Placeholders
|
||||
1. **Topic sections** (scope, exclusion, terms, warranty, delivery)
|
||||
2. **Extra topics** (dynamic topics)
|
||||
3. **Approval signatures**
|
||||
|
||||
### 9.2 Missing Transform Functions
|
||||
1. `formatTopicItems()` - converts topic items to `string[][]`
|
||||
2. `section()` - creates label/data table pairs
|
||||
3. `section2()` - handles extra topics with numbering
|
||||
4. `generateExtraTopicFields()` - dynamic schema generation
|
||||
|
||||
### 9.3 Data Shape Mismatches
|
||||
1. **Tables**: Legacy expects `string[][]`, current may return object arrays
|
||||
2. **Price table**: Legacy uses hardcoded 4-column format
|
||||
3. **Topic items**: Legacy uses `.content` field, current may differ
|
||||
|
||||
### 9.4 Missing Product Type Logic
|
||||
- No product type to topic type mapping
|
||||
- All topics treated the same way
|
||||
- Type-specific labels not applied
|
||||
|
||||
### 9.5 Missing Dynamic Schema Generation
|
||||
- Extra topics cannot appear in PDF
|
||||
- Template schema is static
|
||||
- Cannot handle variable number of topics
|
||||
|
||||
### 9.6 Template Mismatch
|
||||
- Current template has `item_topic` placeholder
|
||||
- Legacy uses `data_topic` with different logic
|
||||
- May need template versioning or migration
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommended Fix Plan
|
||||
|
||||
### 10.1 Phase 1: Add Missing Transform Functions
|
||||
**Priority**: HIGH
|
||||
|
||||
Create utility functions in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
|
||||
```typescript
|
||||
export function formatTopicItems(topic: { items?: Array<{ content?: string }> }): string[][] {
|
||||
if (!topic?.items?.length) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item) => [item.content || '-']);
|
||||
}
|
||||
|
||||
export function createTopicSection(
|
||||
label: string,
|
||||
topic: { items?: Array<{ content?: string }> }
|
||||
): Record<string, string[][]> {
|
||||
return {
|
||||
[`${label}_label`]: [[label]],
|
||||
[`${label}_data`]: formatTopicItems(topic)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Phase 2: Add Product Type Mapping
|
||||
**Priority**: HIGH
|
||||
|
||||
Create mapping configuration:
|
||||
|
||||
```typescript
|
||||
// src/features/crm/quotations/document/config/topic-mapping.ts
|
||||
const TOPIC_TYPE_MAPPING = {
|
||||
crane: {
|
||||
scope: 'scope_of_work',
|
||||
exclusion: 'exclusion_from_scope_of_supply',
|
||||
payment: 'terms_of_payment_upon_progress_of_each_item',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
dockdoor: {
|
||||
scope: 'dock_scope_of_work',
|
||||
exclusion: 'exclusion_of_supply_installation',
|
||||
payment: 'payment_conditions',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
solarcell: {
|
||||
scope: 'solarcell_scope_of_work',
|
||||
exclusion: 'solarcell_exclusion_of_supply_installation',
|
||||
payment: 'solarcell_payment_conditions',
|
||||
delivery: 'solarcell_delivery',
|
||||
warranty: 'solarcell_warranty'
|
||||
},
|
||||
default: {
|
||||
scope: 'scope_of_work',
|
||||
exclusion: 'exclusion_from_supply_installation',
|
||||
payment: 'payment_conditions',
|
||||
delivery: 'delivery',
|
||||
warranty: 'warranty'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 10.3 Phase 3: Update Document Data Builder
|
||||
**Priority**: HIGH
|
||||
|
||||
Modify `buildQuotationDocumentData()` in `document/server/service.ts`:
|
||||
|
||||
1. Add product type detection from quotation
|
||||
2. Split topics into mapped and extra categories
|
||||
3. Format topic items as `string[][]`
|
||||
4. Add approval/signature data
|
||||
|
||||
### 10.4 Phase 4: Fix Table Data Formatting
|
||||
**Priority**: MEDIUM
|
||||
|
||||
Update `mapDocumentDataToTemplateInput()` in `document-template/server/service.ts`:
|
||||
|
||||
```typescript
|
||||
if (mapping.dataType === 'table') {
|
||||
const rows = Array.isArray(rawValue) ? rawValue : [];
|
||||
|
||||
// Check if it's already string[][] format
|
||||
if (rows.length > 0 && Array.isArray(rows[0])) {
|
||||
result[mapping.placeholderKey] = rows;
|
||||
} else {
|
||||
// Convert objects to string[][]
|
||||
result[mapping.placeholderKey] = rows.map((row) => {
|
||||
const rowRecord = row as Record<string, unknown>;
|
||||
return mapping.columns.reduce<string[]>((acc, col) => {
|
||||
acc.push(String(applyFormatMask(rowRecord[col.sourceField], col.formatMask) || ''));
|
||||
return acc;
|
||||
}, []);
|
||||
});
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
### 10.5 Phase 5: Update Template Mappings
|
||||
**Priority**: HIGH
|
||||
|
||||
Add mappings for missing placeholders in database:
|
||||
|
||||
1. `exclusion_label` - static text
|
||||
2. `exclusion_data` - topic data as table
|
||||
3. `Terms_of_payment` - static text
|
||||
4. `data_term_of_payment` - topic data as table
|
||||
5. `warranty` - static text
|
||||
6. `data_warranty` - topic data as table
|
||||
7. `Delivery` - static text
|
||||
8. `data_delivery` - topic data as table
|
||||
9. `Affter service` - static text
|
||||
10. `data_affter_service` - topic data as table
|
||||
11. `data_topic` - general topic data
|
||||
12. Approval fields
|
||||
|
||||
### 10.6 Phase 6: Add Approval Signature Data
|
||||
**Priority**: MEDIUM
|
||||
|
||||
Extract approval workflow data and map to signature fields:
|
||||
- `app1` = Sales Manager (from approval timeline)
|
||||
- `app2` = Department Manager (from approval timeline)
|
||||
- `app3` = Top Manager (from approval timeline)
|
||||
- Position fields from user profiles or roles
|
||||
|
||||
### 10.7 Phase 7: Template Verification
|
||||
**Priority**: HIGH
|
||||
|
||||
Verify current template matches legacy:
|
||||
1. Check all placeholder names match
|
||||
2. Verify field positions match
|
||||
3. Ensure font names match (`cordia`, `cordiaBold`)
|
||||
4. Validate table structures
|
||||
|
||||
### 10.8 Phase 8: Dynamic Schema Generation
|
||||
**Priority**: LOW (Future Enhancement)
|
||||
|
||||
Implement dynamic schema generation for extra topics:
|
||||
1. Generate PDFME field definitions
|
||||
2. Inject into template schema
|
||||
3. Calculate positioning dynamically
|
||||
|
||||
---
|
||||
|
||||
## 11. Risk / Notes
|
||||
|
||||
### 11.1 Risks
|
||||
1. **Template Compatibility**: Current template may have different placeholder names or structure
|
||||
2. **Data Migration**: Existing quotations may not have topic data in correct format
|
||||
3. **Backward Compatibility**: Changes may break existing PDF generation
|
||||
4. **Font Issues**: Cordia fonts may not be available in production
|
||||
|
||||
### 11.2 Notes
|
||||
1. **Legacy Template Location**:
|
||||
- ALLA: `src/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json`
|
||||
- ONVALLA: `src/features/quotations/pdfme/ONVALLA_template_pdfme_fainal3.json`
|
||||
2. **Current Template**: `src/pdfme_template/ALLA_template_pdfme_fainal3.json`
|
||||
3. **Template Version**: Current template may be different version from legacy
|
||||
4. **Topic Data Model**: Current topics structure may need adjustment to match legacy expectations
|
||||
5. **Approval Data**: Current approval system is more complex than legacy, need to map correctly
|
||||
|
||||
### 11.3 Recommendations
|
||||
1. Create a migration script to convert existing topic data to new format
|
||||
2. Implement comprehensive PDF preview to verify all fields
|
||||
3. Add unit tests for each transform function
|
||||
4. Document all placeholder mappings in a central configuration
|
||||
5. Consider versioning PDFME templates to handle changes
|
||||
|
||||
---
|
||||
|
||||
## 12. Conclusion
|
||||
|
||||
The legacy PDFME implementation uses a specialized, direct mapping approach with inline transforms that ensure correct data formatting. The current implementation uses a more generic mapping system that, while more flexible, is missing critical transform logic and placeholder mappings.
|
||||
|
||||
**Primary Issues**:
|
||||
1. Missing topic section mappings (scope, exclusion, terms, warranty, delivery)
|
||||
2. Missing `formatTopicItems()` transform function
|
||||
3. No support for extra topics
|
||||
4. No product type to topic type mapping
|
||||
5. Template placeholder mismatch (`item_topic` vs `data_topic`)
|
||||
6. Missing approval signature mappings
|
||||
|
||||
**Estimated Effort**: 2-3 days to implement all fixes and test thoroughly.
|
||||
|
||||
**Next Steps**:
|
||||
1. Implement transform functions (Phase 1)
|
||||
2. Add topic mappings (Phase 2-3)
|
||||
3. Update template mappings in database (Phase 5)
|
||||
4. Test with real quotation data
|
||||
5. Compare output with legacy PDFs
|
||||
910
docs/implementation/pdfme-runtime-audit.md
Normal file
910
docs/implementation/pdfme-runtime-audit.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# PDFME Runtime Audit Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This audit compares the current PDFME template and mapping system against the legacy implementation and identifies runtime issues that cause incomplete or incorrectly mapped data in generated quotation PDFs.
|
||||
|
||||
**Key Finding**: The current implementation has significant gaps in placeholder mappings, missing topic sections, and incorrect data formats that prevent PDFME from displaying complete quotation information.
|
||||
|
||||
**Total Template Placeholders**: 20
|
||||
**Total Required Mappings**: 20
|
||||
**Missing Mappings**: 8
|
||||
**Critical Issues**: 5
|
||||
|
||||
---
|
||||
|
||||
## 1. Template Analysis
|
||||
|
||||
### 1.1 Current Template File
|
||||
**Path**: `src/pdfme_template/ALLA_template_pdfme_fainal3.json`
|
||||
|
||||
### 1.2 Placeholder Extraction
|
||||
|
||||
The following placeholders were extracted from the current template schema:
|
||||
|
||||
| Placeholder Key | Field Type | Content Pattern | Position (Page 1) |
|
||||
|-----------------|------------|-----------------|-------------------|
|
||||
| `customer_name` | text | `{customer_name}` | Header |
|
||||
| `customer_addr` | text | `{customer_addr}` | Header |
|
||||
| `customer_tel` | text | `{customer_tel}` | Header |
|
||||
| `customer_email` | text | `{customer_email}` | Header |
|
||||
| `customer_att` | text | `{customer_att}` | Header |
|
||||
| `project_name` | text | `{project_name}` | Header |
|
||||
| `site_location` | text | `{site_location}` | Header |
|
||||
| `quotation_date` | text | `{quotation_date}` | Header |
|
||||
| `quotation_code` | text | `{quotation_code}` | Header |
|
||||
| `quotation_price` | table (cell) | `{quotation_price}` | Price section |
|
||||
| `currency` | table (cell) | `{currency}` | Price section |
|
||||
| `exclusion_data` | table | `[[\"{exclusion_data}\"]]` | Exclusion section |
|
||||
|
||||
**Static Content (No Mapping Required)**:
|
||||
- Company header (company1, company2, company_addr1, company_addr2, company_tel, company_email, company_tax)
|
||||
- Labels (Date, Quotation No, Tel, Email, etc.)
|
||||
- Colons and separators
|
||||
- "Dear sirs" text
|
||||
|
||||
---
|
||||
|
||||
## 2. Runtime Audit Results
|
||||
|
||||
### 2.1 Placeholder Mapping Status
|
||||
|
||||
| Placeholder | Template Exists | Mapping Exists | Source Path | Expected Type | Actual Type | Status |
|
||||
|-------------|-----------------|----------------|-------------|---------------|-------------|--------|
|
||||
| `customer_name` | ✅ | ✅ | `customer.name` | string | string | ✅ OK |
|
||||
| `customer_addr` | ✅ | ✅ | `customer.address` | string | string | ✅ OK |
|
||||
| `customer_tel` | ✅ | ✅ | `customer.contacts[primary].phone` | string | string | ✅ OK |
|
||||
| `customer_email` | ✅ | ✅ | `customer.contacts[primary].email` | string | string | ✅ OK |
|
||||
| `customer_att` | ✅ | ✅ | `quotation.attention` | string | string | ✅ OK |
|
||||
| `project_name` | ✅ | ✅ | `quotation.projectName` | string | string | ✅ OK |
|
||||
| `site_location` | ✅ | ✅ | `quotation.projectLocation` | string | string | ✅ OK |
|
||||
| `quotation_date` | ✅ | ✅ | `quotation.quotationDate` | string | string | ⚠️ **Check Format** |
|
||||
| `quotation_code` | ✅ | ✅ | `quotation.code` | string | string | ✅ OK |
|
||||
| `quotation_price` | ✅ | ✅ | `quotation.totalAmount` | string (formatted) | number | ⚠️ **Type Mismatch** |
|
||||
| `currency` | ✅ | ✅ | `quotation.currency` | string | string | ✅ OK |
|
||||
| `exclusion_data` | ✅ | ❌ | - | `string[][]` | undefined | ❌ **MISSING** |
|
||||
|
||||
### 2.2 Legacy Placeholders Not in Current Template
|
||||
|
||||
The following placeholders existed in the legacy implementation but are **not present** in the current template:
|
||||
|
||||
| Placeholder | Legacy Purpose | Status |
|
||||
|-------------|----------------|--------|
|
||||
| `exclusion_label` | Topic label (static) | ❌ **Not in template** |
|
||||
| `Terms_of_payment` | Payment topic label | ❌ **Not in template** |
|
||||
| `data_term_of_payment` | Payment topic data | ❌ **Not in template** |
|
||||
| `warranty` | Warranty topic label | ❌ **Not in template** |
|
||||
| `data_warranty` | Warranty topic data | ❌ **Not in template** |
|
||||
| `Delivery` | Delivery topic label | ❌ **Not in template** |
|
||||
| `data_delivery` | Delivery topic data | ❌ **Not in template** |
|
||||
| `Affter service` | After-service topic label | ❌ **Not in template** |
|
||||
| `data_affter_service` | After-service topic data | ❌ **Not in template** |
|
||||
| `topic` | General topic label | ❌ **Not in template** |
|
||||
| `data_topic` | General topic data | ❌ **Not in template** |
|
||||
| `app1` | Approver 1 name | ❌ **Not in template** |
|
||||
| `app1_position` | Approver 1 position | ❌ **Not in template** |
|
||||
| `app2` | Approver 2 name | ❌ **Not in template** |
|
||||
| `app2_position` | Approver 2 position | ❌ **Not in template** |
|
||||
| `app3` | Approver 3 name | ❌ **Not in template** |
|
||||
| `app3_position` | Approver 3 position | ❌ **Not in template** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical Issues
|
||||
|
||||
### 3.1 Issue: Missing Exclusion Data Mapping
|
||||
|
||||
**Severity**: HIGH
|
||||
**Placeholder**: `exclusion_data`
|
||||
**Template Format**: `[[\"{exclusion_data}\"]]` (table with single column)
|
||||
**Expected Data Type**: `string[][]`
|
||||
**Current Status**: No mapping exists
|
||||
|
||||
**Problem**:
|
||||
- Template expects exclusion topic items to be displayed as a table
|
||||
- Legacy implementation used `formatTopicItems()` to convert topic items to `string[][]` format
|
||||
- Current implementation has no mapping for this placeholder
|
||||
- Exclusion topic data exists in database but is not passed to PDFME
|
||||
|
||||
**Impact**:
|
||||
- Exclusion section will be empty or show placeholder text
|
||||
- Critical business information missing from quotation PDF
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
// Legacy: src/features/quotations/lib/pdfme-export.ts
|
||||
function formatTopicItems(topic: any): string[][] {
|
||||
if (!topic || !topic.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item: any) => [item.content || '-']);
|
||||
}
|
||||
|
||||
const termsTopic = topics.find(t => t.topicType === 'exclusion_from_scope_of_supply');
|
||||
data.exclusion_data = formatTopicItems(termsTopic);
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add mapping for `exclusion_data` placeholder
|
||||
2. Transform topic items to `string[][]` format
|
||||
3. Handle empty topics with fallback `[['-']]`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Issue: Missing Topic Sections
|
||||
|
||||
**Severity**: HIGH
|
||||
**Affected Placeholders**:
|
||||
- `Terms_of_payment`, `data_term_of_payment`
|
||||
- `warranty`, `data_warranty`
|
||||
- `Delivery`, `data_delivery`
|
||||
- `Affter service`, `data_affter_service`
|
||||
|
||||
**Problem**:
|
||||
- Legacy template had separate sections for each topic type
|
||||
- Current template only has exclusion section
|
||||
- Payment, warranty, delivery, and after-service topics are not displayed
|
||||
- These are critical business terms in quotations
|
||||
|
||||
**Impact**:
|
||||
- Payment terms not visible to customer
|
||||
- Warranty information missing
|
||||
- Delivery details not specified
|
||||
- After-service information absent
|
||||
|
||||
**Legacy Structure**:
|
||||
```
|
||||
Topic: Scope of Work
|
||||
[data_scope]
|
||||
|
||||
Topic: Exclusion from scope of Supply:
|
||||
[data_exclusion]
|
||||
|
||||
Terms of payment upon progress of each item:
|
||||
[data_term_of_payment]
|
||||
|
||||
Warranty:
|
||||
[data_warranty]
|
||||
|
||||
Delivery:
|
||||
[data_delivery]
|
||||
|
||||
Affter service:
|
||||
[data_affter_service]
|
||||
```
|
||||
|
||||
**Current Template Structure**:
|
||||
```
|
||||
Quotation details...
|
||||
|
||||
Exclusion from scope of Supply:
|
||||
[exclusion_data]
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add topic section fields to template OR
|
||||
2. Merge all topics into a single table with labels
|
||||
3. Implement topic filtering based on product type
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Issue: Currency Format Mismatch
|
||||
|
||||
**Severity**: MEDIUM
|
||||
**Placeholder**: `quotation_price`
|
||||
**Template Usage**: `[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]`
|
||||
**Expected Type**: Formatted string (e.g., "฿1,000,000.00")
|
||||
**Actual Type**: Number (from database)
|
||||
|
||||
**Problem**:
|
||||
- Database stores `quotation.totalAmount` as number (double precision)
|
||||
- PDFME table cell expects formatted currency string
|
||||
- Current mapping likely passes raw number without formatting
|
||||
- Thai locale formatting required (฿ symbol, thousand separators, 2 decimal places)
|
||||
|
||||
**Impact**:
|
||||
- Price may display as "1000000" instead of "฿1,000,000.00"
|
||||
- Unprofessional appearance
|
||||
- Potential customer confusion
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
function formatCurrency(amount: number, currencyCode: CurrencyCode): string {
|
||||
const symbol = getCurrencySymbol(currencyCode);
|
||||
const formatted = new Intl.NumberFormat('th-TH', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
|
||||
data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add currency formatting transform
|
||||
2. Use `formatMask` in mapping or custom transform function
|
||||
3. Support multiple currency codes
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Issue: Date Format
|
||||
|
||||
**Severity**: MEDIUM
|
||||
**Placeholder**: `quotation_date`
|
||||
**Expected Format**: Localized date string (Thai: Buddhist calendar or English: Gregorian)
|
||||
**Current Format**: ISO date string
|
||||
|
||||
**Problem**:
|
||||
- Database stores dates as ISO 8601 strings (e.g., "2026-06-17T00:00:00.000Z")
|
||||
- PDF displays raw ISO string instead of formatted date
|
||||
- Legacy implementation had language-aware date formatting
|
||||
|
||||
**Impact**:
|
||||
- Unprofessional appearance
|
||||
- May cause confusion about quotation validity date
|
||||
- Doesn't match business requirements for Thai/English date formats
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
function formatDate(dateString: string, language: 'th' | 'en'): string {
|
||||
const date = new Date(dateString);
|
||||
if (language === 'th') {
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
calendar: 'buddhist'
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add date formatting transform
|
||||
2. Support Thai and English locales
|
||||
3. Configure format mask in mapping
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Issue: Missing Product Type Topic Mapping
|
||||
|
||||
**Severity**: HIGH
|
||||
**Legacy Behavior**: Different topic types mapped based on quotation product type
|
||||
|
||||
**Problem**:
|
||||
- Legacy implementation had product-specific topic mappings:
|
||||
- **Crane**: scope_of_work, exclusion_from_scope_of_supply, terms_of_payment_upon_progress_of_each_item, delivery_date, warranty
|
||||
- **Dockdoor**: dock_scope_of_work, exclusion_of_supply_installation, payment_conditions, delivery_date, warranty
|
||||
- **Solarcell**: solarcell_scope_of_work, solarcell_exclusion_of_supply_installation, solarcell_payment_conditions, solarcell_delivery, solarcell_warranty
|
||||
- Current implementation treats all topics uniformly
|
||||
- Topic type labels may not match product type
|
||||
|
||||
**Impact**:
|
||||
- Wrong topic labels for different product types
|
||||
- Inconsistent quotation formatting
|
||||
- Business process violation
|
||||
|
||||
**Required Fix**:
|
||||
1. Implement product type to topic type mapping
|
||||
2. Apply correct labels based on quotation type
|
||||
3. Support dynamic topic resolution
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Format Analysis
|
||||
|
||||
### 4.1 Table Data Formats
|
||||
|
||||
#### Exclusion Data Table
|
||||
|
||||
**Template Content**:
|
||||
```json
|
||||
"content": "[[\"{exclusion_data}\"]]"
|
||||
```
|
||||
|
||||
**Expected Format**: `string[][]`
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
[
|
||||
["Provide crane and installation service"],
|
||||
["Site survey and installation planning"],
|
||||
["Operator training (2 days)"]
|
||||
]
|
||||
```
|
||||
|
||||
**Legacy Transform**:
|
||||
```typescript
|
||||
function formatTopicItems(topic: any): string[][] {
|
||||
if (!topic || !topic.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item: any) => [item.content || '-']);
|
||||
}
|
||||
```
|
||||
|
||||
**Current Status**: ❌ Not implemented
|
||||
|
||||
---
|
||||
|
||||
#### Price Table
|
||||
|
||||
**Template Content**:
|
||||
```json
|
||||
"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]"
|
||||
```
|
||||
|
||||
**Expected Format**: `string[][]` (4 columns)
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
[
|
||||
["Price (Exclude VAT)", "฿1,000,000.00", " ", "THB"]
|
||||
]
|
||||
```
|
||||
|
||||
**Legacy Transform**:
|
||||
```typescript
|
||||
data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);
|
||||
data.currency = quotation.currency;
|
||||
```
|
||||
|
||||
**Current Status**: ⚠️ Number passed instead of formatted string
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Text Data Formats
|
||||
|
||||
#### Customer Contact Information
|
||||
|
||||
**Legacy Prefixes**:
|
||||
- Tel: `Tel : {customer_tel}`
|
||||
- Email: `Email : {customer_email}`
|
||||
|
||||
**Current Template**: Labels and data are separate fields
|
||||
- Label: "Tel" (static)
|
||||
- Data: `{customer_tel}` (placeholder)
|
||||
|
||||
**Status**: ✅ OK (modern approach)
|
||||
|
||||
---
|
||||
|
||||
## 5. Current Implementation Gaps
|
||||
|
||||
### 5.1 Missing Transform Functions
|
||||
|
||||
| Function | Purpose | Current Status |
|
||||
|----------|---------|----------------|
|
||||
| `formatTopicItems()` | Convert topic items to `string[][]` | ❌ Missing |
|
||||
| `formatCurrency()` | Format currency with locale | ⚠️ May exist but not applied |
|
||||
| `formatDate()` | Format date with locale | ⚠️ May exist but not applied |
|
||||
| `getCurrencySymbol()` | Get currency symbol | ❌ Missing |
|
||||
| `section()` | Create label/data pairs for topics | ❌ Missing |
|
||||
| `productTypeTopicMapping` | Map product type to topic types | ❌ Missing |
|
||||
|
||||
### 5.2 Missing Database Mappings
|
||||
|
||||
The following mappings should exist in `crm_document_template_mappings` table:
|
||||
|
||||
| placeholder_key | source_path | data_type | format_mask | status |
|
||||
|-----------------|-------------|-----------|-------------|--------|
|
||||
| `exclusion_data` | `topics.exclusion_from_scope_of_supply.items` | table | - | ❌ Missing |
|
||||
| `quotation_price` | `quotation.totalAmount` | scalar | currency_THB | ⚠️ Needs format |
|
||||
| `quotation_date` | `quotation.quotationDate` | scalar | date_th | ⚠️ Needs format |
|
||||
|
||||
### 5.3 Data Builder Issues
|
||||
|
||||
**File**: `src/features/crm/quotations/document/server/service.ts`
|
||||
|
||||
**Issues**:
|
||||
1. `buildQuotationDocumentData()` does not transform topic items to `string[][]`
|
||||
2. Does not format currency or dates
|
||||
3. Does not apply product type topic mapping
|
||||
4. Does not handle empty topics with fallbacks
|
||||
|
||||
---
|
||||
|
||||
## 6. Comparison with Legacy
|
||||
|
||||
### 6.1 Legacy Flow
|
||||
|
||||
```typescript
|
||||
generateQuotationPDF(quotation, customer, topics)
|
||||
↓
|
||||
mapDataToTemplate() // Direct mapping with transforms
|
||||
↓
|
||||
formatDate() // Thai/English date
|
||||
↓
|
||||
formatCurrency() // Thai locale currency
|
||||
↓
|
||||
formatTopicItems() // Convert to string[][]
|
||||
↓
|
||||
generateExtraTopicFields() // Dynamic schema
|
||||
↓
|
||||
pdfme.generate(template, inputs)
|
||||
```
|
||||
|
||||
### 6.2 Current Flow
|
||||
|
||||
```typescript
|
||||
buildQuotationDocumentData() // Fetch data
|
||||
↓
|
||||
mapDocumentDataToTemplateInput() // Generic mapping
|
||||
↓
|
||||
applyFormatMask() // Simple format masks only
|
||||
↓
|
||||
pdfme.generate(template, inputs)
|
||||
```
|
||||
|
||||
### 6.3 Key Differences
|
||||
|
||||
| Aspect | Legacy | Current | Gap |
|
||||
|--------|--------|---------|-----|
|
||||
| Mapping Approach | Direct with inline transforms | Generic via database | Transform logic |
|
||||
| Topic Formatting | `string[][]` via `formatTopicItems()` | Unknown | Format mismatch |
|
||||
| Currency Formatting | `formatCurrency()` with locale | Basic format mask | Missing locale |
|
||||
| Date Formatting | `formatDate()` with locale | Basic format mask | Missing locale |
|
||||
| Product Type | Product-specific topic mapping | None | Wrong labels |
|
||||
| Dynamic Topics | `generateExtraTopicFields()` | Static schema | No extra topics |
|
||||
| Fallback Values | Inline defaults in mapping | `defaultValue` column | May not cover all cases |
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommended Fixes
|
||||
|
||||
### 7.1 Immediate Fixes (P0)
|
||||
|
||||
#### Fix 1: Add Exclusion Data Mapping
|
||||
|
||||
**Add to `crm_document_template_mappings`**:
|
||||
```sql
|
||||
INSERT INTO crm_document_template_mappings (
|
||||
id, organization_id, template_version_id, placeholder_key,
|
||||
source_path, data_type, sort_order
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
'<organization_id>',
|
||||
'<template_version_id>',
|
||||
'exclusion_data',
|
||||
'topics.exclusion_from_scope_of_supply.items',
|
||||
'table',
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
**Add transform function** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatTopicItems(
|
||||
topic: { items?: Array<{ content?: string }> } | undefined
|
||||
): string[][] {
|
||||
if (!topic?.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item) => [item.content || '-']);
|
||||
}
|
||||
```
|
||||
|
||||
**Update `buildQuotationDocumentData()`**:
|
||||
```typescript
|
||||
const exclusionTopic = topics.find(
|
||||
t => t.topicType === 'exclusion_from_scope_of_supply'
|
||||
);
|
||||
data.exclusion_data = formatTopicItems(exclusionTopic);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Fix 2: Format Currency
|
||||
|
||||
**Update mapping for `quotation_price`**:
|
||||
```sql
|
||||
UPDATE crm_document_template_mappings
|
||||
SET format_mask = 'currency_THB'
|
||||
WHERE placeholder_key = 'quotation_price';
|
||||
```
|
||||
|
||||
**Add currency formatter** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currencyCode: string = 'THB'
|
||||
): string {
|
||||
const symbols: Record<string, string> = {
|
||||
THB: '฿',
|
||||
USD: '$',
|
||||
EUR: '€'
|
||||
};
|
||||
const symbol = symbols[currencyCode] || currencyCode;
|
||||
const formatted = new Intl.NumberFormat('th-TH', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
```
|
||||
|
||||
**Update `mapDocumentDataToTemplateInput()`** in `document-template/server/service.ts`:
|
||||
```typescript
|
||||
if (mapping.formatMask?.startsWith('currency_')) {
|
||||
const currencyCode = mapping.formatMask.replace('currency_', '');
|
||||
result[mapping.placeholderKey] = formatCurrency(
|
||||
rawValue as number,
|
||||
currencyCode
|
||||
);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Fix 3: Format Date
|
||||
|
||||
**Update mapping for `quotation_date`**:
|
||||
```sql
|
||||
UPDATE crm_document_template_mappings
|
||||
SET format_mask = 'date_th'
|
||||
WHERE placeholder_key = 'quotation_date';
|
||||
```
|
||||
|
||||
**Add date formatter** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatDate(
|
||||
dateString: string,
|
||||
language: 'th' | 'en' = 'th'
|
||||
): string {
|
||||
const date = new Date(dateString);
|
||||
if (language === 'th') {
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
calendar: 'buddhist'
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Update `mapDocumentDataToTemplateInput()`**:
|
||||
```typescript
|
||||
if (mapping.formatMask?.startsWith('date_')) {
|
||||
const language = mapping.formatMask.replace('date_', '') as 'th' | 'en';
|
||||
result[mapping.placeholderKey] = formatDate(
|
||||
rawValue as string,
|
||||
language
|
||||
);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Template Updates (P0)
|
||||
|
||||
#### Option A: Add Missing Topic Sections to Template
|
||||
|
||||
Add the following fields to the template schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "payment_label",
|
||||
"type": "table",
|
||||
"content": "[[\"Terms of payment upon progress of each item:\"]]",
|
||||
"position": { "x": 10, "y": 220 },
|
||||
"width": 190,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"name": "payment_data",
|
||||
"type": "table",
|
||||
"content": "[[\"{payment_data}\"]]",
|
||||
"position": { "x": 15, "y": 230 },
|
||||
"width": 185,
|
||||
"height": 7
|
||||
},
|
||||
{
|
||||
"name": "warranty_label",
|
||||
"type": "table",
|
||||
"content": "[[\"Warranty:\"]]",
|
||||
"position": { "x": 10, "y": 250 },
|
||||
"width": 190,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"name": "warranty_data",
|
||||
"type": "table",
|
||||
"content": "[[\"{warranty_data}\"]]",
|
||||
"position": { "x": 15, "y": 260 },
|
||||
"width": 185,
|
||||
"height": 7
|
||||
}
|
||||
```
|
||||
|
||||
#### Option B: Consolidate Topics into Single Table
|
||||
|
||||
Create a unified topics table with labels:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "topics_table",
|
||||
"type": "table",
|
||||
"content": "[{topics_data}]",
|
||||
"position": { "x": 10, "y": 195 },
|
||||
"width": 190,
|
||||
"height": 50
|
||||
}
|
||||
```
|
||||
|
||||
Where `topics_data` is a structured array:
|
||||
```typescript
|
||||
[
|
||||
["Exclusion from scope of Supply:", "Provide crane, Site survey"],
|
||||
["Terms of payment:", "30% deposit, 70% on delivery"],
|
||||
["Warranty:", "12 months parts and labor"],
|
||||
["Delivery:", "30 days after order confirmation"],
|
||||
["After service:", "Annual maintenance available"]
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.3 Product Type Mapping (P1)
|
||||
|
||||
**Create configuration** in `src/features/crm/quotations/document/config/topic-mapping.ts`:
|
||||
```typescript
|
||||
export const TOPIC_TYPE_MAPPING: Record<string, Record<string, string>> = {
|
||||
crane: {
|
||||
scope: 'scope_of_work',
|
||||
exclusion: 'exclusion_from_scope_of_supply',
|
||||
payment: 'terms_of_payment_upon_progress_of_each_item',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
dockdoor: {
|
||||
scope: 'dock_scope_of_work',
|
||||
exclusion: 'exclusion_of_supply_installation',
|
||||
payment: 'payment_conditions',
|
||||
delivery: 'delivery_date',
|
||||
warranty: 'warranty'
|
||||
},
|
||||
solarcell: {
|
||||
scope: 'solarcell_scope_of_work',
|
||||
exclusion: 'solarcell_exclusion_of_supply_installation',
|
||||
payment: 'solarcell_payment_conditions',
|
||||
delivery: 'solarcell_delivery',
|
||||
warranty: 'solarcell_warranty'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Update `buildQuotationDocumentData()`** to use product type:
|
||||
```typescript
|
||||
const mapping = TOPIC_TYPE_MAPPING[quotation.productType] || TOPIC_TYPE_MAPPING.crane;
|
||||
const exclusionTopic = topics.find(t => t.topicType === mapping.exclusion);
|
||||
data.exclusion_data = formatTopicItems(exclusionTopic);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Risk Assessment
|
||||
|
||||
### 8.1 High Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing exclusion data | High customer confusion | 100% | Add mapping immediately |
|
||||
| Missing payment terms | Legal/compliance issue | 100% | Add to template |
|
||||
| Wrong currency format | Unprofessional appearance | 90% | Add currency formatter |
|
||||
| Wrong date format | Date confusion | 90% | Add date formatter |
|
||||
|
||||
### 8.2 Medium Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing product type mapping | Inconsistent formatting | 80% | Implement product mapping |
|
||||
| Missing warranty info | Customer expectations | 70% | Add to template |
|
||||
| Missing delivery info | Order confusion | 70% | Add to template |
|
||||
|
||||
### 8.3 Low Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing signatures | Process compliance | 50% | Add approval workflow |
|
||||
| Static company info | Outdated info | 30% | Make dynamic |
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Recommendations
|
||||
|
||||
### 9.1 Unit Tests
|
||||
|
||||
Create tests for transform functions:
|
||||
```typescript
|
||||
// tests/features/crm/quotations/document/utils.test.ts
|
||||
describe('formatTopicItems', () => {
|
||||
it('should format topic items to string[][]', () => {
|
||||
const topic = {
|
||||
items: [
|
||||
{ content: 'Item 1' },
|
||||
{ content: 'Item 2' }
|
||||
]
|
||||
};
|
||||
expect(formatTopicItems(topic)).toEqual([
|
||||
['Item 1'],
|
||||
['Item 2']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback for empty topic', () => {
|
||||
expect(formatTopicItems(undefined)).toEqual([['-']]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 Integration Tests
|
||||
|
||||
Test end-to-end PDF generation:
|
||||
```typescript
|
||||
describe('Quotation PDF Generation', () => {
|
||||
it('should generate PDF with all fields populated', async () => {
|
||||
const quotation = await createTestQuotation();
|
||||
const pdf = await generateQuotationPDF(quotation.id);
|
||||
|
||||
// Verify placeholders are replaced
|
||||
expect(pdf).toContain('CUSTOMER NAME');
|
||||
expect(pdf).not.toContain('{customer_name}');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 Visual Regression Tests
|
||||
|
||||
Compare generated PDFs with legacy PDFs:
|
||||
1. Generate PDF from legacy implementation
|
||||
2. Generate PDF from current implementation
|
||||
3. Compare pixel-by-pixel
|
||||
4. Identify visual differences
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation Priority
|
||||
|
||||
### Phase 1: Critical Fixes (1-2 days)
|
||||
1. ✅ Add exclusion data mapping
|
||||
2. ✅ Implement `formatTopicItems()`
|
||||
3. ✅ Format currency
|
||||
4. ✅ Format dates
|
||||
5. ✅ Test with real quotation
|
||||
|
||||
### Phase 2: Template Updates (1 day)
|
||||
1. Add missing topic sections (payment, warranty, delivery)
|
||||
2. Update template layout
|
||||
3. Add mappings for new fields
|
||||
|
||||
### Phase 3: Product Type Logic (1 day)
|
||||
1. Implement product type mapping
|
||||
2. Update topic resolution
|
||||
3. Test different product types
|
||||
|
||||
### Phase 4: Enhancements (2-3 days)
|
||||
1. Dynamic topic schema generation
|
||||
2. Approval signature integration
|
||||
3. Visual regression testing
|
||||
|
||||
---
|
||||
|
||||
## 11. Conclusion
|
||||
|
||||
The current PDFME implementation has significant gaps compared to the legacy system, resulting in incomplete or incorrectly formatted quotation PDFs. The primary issues are:
|
||||
|
||||
1. **Missing topic mappings** (exclusion, payment, warranty, delivery)
|
||||
2. **Missing transform functions** (formatTopicItems, currency, date)
|
||||
3. **Template structure differences** (fewer sections)
|
||||
4. **No product type logic** (generic topic labels)
|
||||
|
||||
**Recommended Action**: Implement Phase 1 fixes immediately to restore basic functionality, then proceed with template updates and product type logic.
|
||||
|
||||
**Estimated Total Effort**: 5-7 days for complete resolution.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Placeholder Inventory
|
||||
|
||||
### A.1 Current Template Placeholders
|
||||
|
||||
```
|
||||
customer_name
|
||||
customer_addr
|
||||
customer_tel
|
||||
customer_email
|
||||
customer_att
|
||||
project_name
|
||||
site_location
|
||||
quotation_date
|
||||
quotation_code
|
||||
quotation_price
|
||||
currency
|
||||
exclusion_data
|
||||
```
|
||||
|
||||
### A.2 Legacy Template Placeholders (Not in Current)
|
||||
|
||||
```
|
||||
exclusion_label
|
||||
Terms_of_payment
|
||||
data_term_of_payment
|
||||
warranty
|
||||
data_warranty
|
||||
Delivery
|
||||
data_delivery
|
||||
Affter service
|
||||
data_affter_service
|
||||
topic
|
||||
data_topic
|
||||
app1
|
||||
app1_position
|
||||
app2
|
||||
app2_position
|
||||
app3
|
||||
app3_position
|
||||
```
|
||||
|
||||
### A.3 Mapped vs Unmapped
|
||||
|
||||
| Category | Total | Mapped | Unmapped | % Mapped |
|
||||
|----------|-------|--------|----------|----------|
|
||||
| Customer Info | 4 | 4 | 0 | 100% |
|
||||
| Quotation Info | 6 | 6 | 0 | 100% |
|
||||
| Topics | 1 | 0 | 1 | 0% |
|
||||
| Signatures | 6 | 0 | 6 | 0% |
|
||||
| **Total** | **17** | **10** | **7** | **59%** |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Data Source Reference
|
||||
|
||||
### B.1 Database Schema
|
||||
|
||||
**Customer**: `crm_customers`
|
||||
- `name` → `{customer_name}`
|
||||
- `address` → `{customer_addr}`
|
||||
- `phone` → `{customer_tel}`
|
||||
- `email` → `{customer_email}`
|
||||
|
||||
**Quotation**: `crm_quotations`
|
||||
- `attention` → `{customer_att}`
|
||||
- `projectName` → `{project_name}`
|
||||
- `projectLocation` → `{site_location}`
|
||||
- `quotationDate` → `{quotation_date}`
|
||||
- `code` → `{quotation_code}`
|
||||
- `totalAmount` → `{quotation_price}`
|
||||
- `currency` → `{currency}`
|
||||
|
||||
**Topics**: `crm_quotation_topics` + `crm_quotation_topic_items`
|
||||
- `topicType` → determines which topic
|
||||
- `items[].content` → topic content items
|
||||
|
||||
### B.2 Source Path Examples
|
||||
|
||||
| Placeholder | Source Path | Notes |
|
||||
|-------------|-------------|-------|
|
||||
| `customer_name` | `customer.name` | Direct mapping |
|
||||
| `customer_tel` | `customer.contacts[primary].phone` | Array access |
|
||||
| `quotation_price` | `quotation.totalAmount` | Number → string transform |
|
||||
| `exclusion_data` | `topics.exclusion_from_scope_of_supply.items` | Topic → string[][] transform |
|
||||
|
||||
---
|
||||
|
||||
**Report Generated**: 2026-06-17
|
||||
**Auditor**: AI Code Review Agent
|
||||
**Version**: 1.0
|
||||
@@ -0,0 +1,56 @@
|
||||
# Quotation Create 400 Investigation - 2026-06-30
|
||||
|
||||
## Scope
|
||||
Investigate why quotation creation fails with `POST /api/crm/quotations -> 400`.
|
||||
|
||||
## Summary
|
||||
The failure is reproducible on the running local app and the API returns:
|
||||
|
||||
```json
|
||||
{"message":"Missing product type code configuration quotation type."}
|
||||
```
|
||||
|
||||
This happens inside the document-sequence layer during quotation code generation, not in the quotation form schema itself.
|
||||
|
||||
## Reproduction
|
||||
1. Sign in with seeded UAT admin account:
|
||||
- `admin.uat@alla.local`
|
||||
- `UatDemo123!`
|
||||
2. Submit a valid-looking quotation payload to `POST /api/crm/quotations`.
|
||||
3. The API returns `400` with message:
|
||||
- `Missing product type code configuration quotation type.`
|
||||
|
||||
Reproduced against the local app at `http://localhost:3000` with authenticated session.
|
||||
|
||||
## Findings
|
||||
1. Quotation creation reaches [createQuotation()](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/server/service.ts:1329), passes quotation payload validation, and then calls `generateNextDocumentCode()` for document code allocation.
|
||||
2. The failure occurs inside the document-sequence product-type resolution path in [src/features/foundation/document-sequence/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/service.ts:152) and [generateNextDocumentCode()](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/service.ts:527).
|
||||
3. Database inspection shows the organization does have valid master options for both:
|
||||
- `crm_quotation_type`: `crane`, `dockdoor`, `solarcell`, `service`, `other`
|
||||
- `crm_product_type`: `crane`, `dockdoor`, `solarcell`, `service`, `sparepart`
|
||||
4. Database inspection also shows quotation document sequences already exist for:
|
||||
- `crane`
|
||||
- `dockdoor`
|
||||
- `solarcell`
|
||||
- `service`
|
||||
5. Because both option categories and document-sequence rows are present, the `400` is not caused by missing seed data in `ms_options` or missing quotation sequence rows.
|
||||
6. The remaining failure point is the runtime mapping logic that resolves a quotation type id/code into the product-type code used by document sequences. That logic is currently rejecting a valid quotation type before sequence generation completes.
|
||||
|
||||
## Likely Root Cause
|
||||
The product-type map resolution in [src/features/foundation/document-sequence/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/service.ts:152) is not successfully resolving the incoming quotation type id/code to a usable document-sequence product type, even though:
|
||||
- the quotation type option exists
|
||||
- the product type option exists
|
||||
- the document sequence row exists
|
||||
|
||||
In other words, this is a mapping bug in the document-sequence resolution path, not a missing reference-data problem in the quotation form.
|
||||
|
||||
## Secondary Observation
|
||||
The frontend currently surfaces quotation create failures through [apiClient](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/api-client.ts:1) as generic `API error: 400 Bad Request`, which hides the backend message unless the request is inspected manually. That makes this class of failure harder to diagnose from the UI alone.
|
||||
|
||||
## Status
|
||||
- Investigation completed
|
||||
- Root cause area identified
|
||||
- No production code fix applied in this investigation round
|
||||
|
||||
## Recommended Next Step
|
||||
Patch the document-sequence product-type resolution so quotation type ids from quotation creation resolve consistently to the corresponding `crm_product_type` code before `generateNextDocumentCode()` runs.
|
||||
426
docs/implementation/task-a-template-audit.md
Normal file
426
docs/implementation/task-a-template-audit.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# Task A Template Audit
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
This repository is already partway through the migration described in the project instructions. The current production-facing baseline is `Auth.js` plus app-owned `users`, `organizations`, and `memberships`, with `products` and `users` already using Route Handlers plus Drizzle. The dashboard still contains a mix of real app patterns and template/demo seams, so Task B should extend only the migrated patterns and avoid inheriting placeholder implementations.
|
||||
|
||||
The convention freeze for ALLA OS CRM vNext should be:
|
||||
|
||||
- Primary tenant boundary is `organizationId`.
|
||||
- Membership, role, and permission checks live inside the active organization context.
|
||||
- `branchId` is a business sub-scope inside an organization, not the tenant key.
|
||||
- New CRM production work should follow the `products` and `users` module architecture, while using `Layout.md` as the structural UI source of truth for CRM pages.
|
||||
|
||||
No required audit path was missing. `Layout.md`, `AGENTS.md`, `README.md`, `package.json`, `src/db/schema.ts`, `src/lib/auth`, `src/app/dashboard`, `src/features`, and `src/components` are all present.
|
||||
|
||||
## 2. Project Structure
|
||||
|
||||
Observed top-level implementation anchors:
|
||||
|
||||
- App Router lives under `src/app`.
|
||||
- Shared UI primitives live under `src/components/ui`.
|
||||
- Layout primitives live under `src/components/layout`.
|
||||
- Feature modules live under `src/features`.
|
||||
- Drizzle schema is centralized in `src/db/schema.ts`.
|
||||
- Shared auth helpers live under `src/lib/auth`.
|
||||
- API boundaries live under `src/app/api/**`.
|
||||
|
||||
Observed feature directories:
|
||||
|
||||
- `auth`
|
||||
- `chat`
|
||||
- `crm`
|
||||
- `elements`
|
||||
- `example-dashboard`
|
||||
- `forms`
|
||||
- `kanban`
|
||||
- `notifications`
|
||||
- `overview`
|
||||
- `products`
|
||||
- `profile`
|
||||
- `react-query-demo`
|
||||
- `setup`
|
||||
- `users`
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- Treat `products` and `users` as the authoritative app-owned CRUD reference.
|
||||
- Treat `crm`, `example-dashboard`, `forms`, `react-query-demo`, and several dashboard demo routes as template/demo seams unless and until they are migrated behind route handlers and auth-aware persistence.
|
||||
|
||||
## 3. Layout.md Findings
|
||||
|
||||
`Layout.md` is a structural skeleton, not a token source. It explicitly says no design tokens, colors, text, or icons are embedded directly and everything is represented by semantic placeholders.
|
||||
|
||||
Important frozen rules from `Layout.md`:
|
||||
|
||||
- CRM detail pages should use Template A.
|
||||
- CRM list pages should use Template B.
|
||||
- Page composition should remain slot-based: header, action row, preview, progress, main column, side column, dialogs.
|
||||
- Layout spacing should map to semantic spacing slots rather than inventing page-local spacing rules.
|
||||
- Cards, tabs, summary grids, timelines, and action rows are part of the intended page grammar.
|
||||
- Detail pages are expected to support status-driven actions, preview panels, and side-column quick info where the business flow needs them.
|
||||
|
||||
Implication for Task B:
|
||||
|
||||
- Build CRM UI by mapping repository components and tokens onto the `Layout.md` skeleton.
|
||||
- Do not invent a new page grammar if the existing template already covers the CRM use case.
|
||||
|
||||
## 4. kiranism-shadcn-dashboard Skill Findings
|
||||
|
||||
The skill is directionally correct but partially stale relative to the current repo state.
|
||||
|
||||
Still valid:
|
||||
|
||||
- Use Auth.js, Drizzle, route handlers, and app-owned org/membership semantics.
|
||||
- Keep feature code under `src/features/<name>/`.
|
||||
- Use `types.ts -> service.ts -> queries.ts -> mutations.ts`.
|
||||
- Use local `apiClient` from feature services.
|
||||
- Prefer server prefetch plus `HydrationBoundary`.
|
||||
|
||||
Stale compared with current code:
|
||||
|
||||
- The skill says there is no committed `auth.ts`, Drizzle schema, or shared RBAC model. Those now exist in `src/auth.ts`, `src/db/schema.ts`, and `src/lib/auth/*`.
|
||||
- The skill says `src/features/products/api/service.ts` and `src/features/users/api/service.ts` still call mock data. They now call local route handlers through `apiClient`.
|
||||
|
||||
Convention freeze:
|
||||
|
||||
- Use the skill for architecture direction.
|
||||
- Use the repository code as the final source of truth when the skill and code disagree.
|
||||
|
||||
## 5. Routing Convention
|
||||
|
||||
Observed routing pattern:
|
||||
|
||||
- Global dashboard shell: `src/app/dashboard/layout.tsx`
|
||||
- Feature pages: `src/app/dashboard/**/page.tsx`
|
||||
- API boundaries: `src/app/api/**/route.ts`
|
||||
- Auth entrypoint: `src/app/api/auth/[...nextauth]/route.ts`
|
||||
|
||||
Dashboard routes currently mix three categories:
|
||||
|
||||
- Migrated app-owned routes: `product`, `users`, `workspaces`
|
||||
- Template utility/demo routes: `forms`, `react-query`, `elements`, `overview`, `kanban`, `notifications`, `billing`, `profile`
|
||||
- CRM demo routes: `crm/**`
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- New CRM production pages should live under `src/app/dashboard/crm/**`.
|
||||
- Production data for CRM must move through `src/app/api/crm/**` or feature-specific route handler namespaces, not direct mock service access.
|
||||
|
||||
## 6. Dashboard Layout Convention
|
||||
|
||||
`src/app/dashboard/layout.tsx` establishes the dashboard shell:
|
||||
|
||||
- `KBar`
|
||||
- `SidebarProvider`
|
||||
- `AppSidebar`
|
||||
- `SidebarInset`
|
||||
- `Header`
|
||||
- `InfobarProvider`
|
||||
- `InfoSidebar`
|
||||
|
||||
`src/components/layout/page-container.tsx` is the standard page wrapper and should remain the default page-level container:
|
||||
|
||||
- Handles title and description through `Heading`
|
||||
- Supports page header actions
|
||||
- Supports access fallback state
|
||||
- Supports loading skeleton state
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- Use `PageContainer` for dashboard pages instead of composing page headers ad hoc.
|
||||
- Preserve the existing shell and only replace inner feature content.
|
||||
|
||||
## 7. Feature Folder Convention
|
||||
|
||||
Strong existing reference pattern:
|
||||
|
||||
- `src/features/products/api/{types,service,queries,mutations}.ts`
|
||||
- `src/features/users/api/{types,service,queries,mutations}.ts`
|
||||
- `src/features/<feature>/components/**`
|
||||
- `src/features/<feature>/schemas/**` when forms exist
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- CRM production work should follow the same split:
|
||||
- `api/types.ts`
|
||||
- `api/service.ts`
|
||||
- `api/queries.ts`
|
||||
- `api/mutations.ts`
|
||||
- `components/**`
|
||||
- `schemas/**` for form validation
|
||||
|
||||
## 8. Component Convention
|
||||
|
||||
Observed shared conventions:
|
||||
|
||||
- Shared UI primitives live under `src/components/ui/**`.
|
||||
- Icons should be imported from `@/components/icons`.
|
||||
- Page headers should go through `PageContainer`.
|
||||
- Contextual documentation uses `InfoSidebar` plus `infoContent`.
|
||||
|
||||
Server/client split:
|
||||
|
||||
- Server components are the default for route entry pages.
|
||||
- Client components are used for interactive tables, forms, switchers, and session-driven sidebar behavior.
|
||||
- `AppSidebar`, `OrgSwitcher`, forms, and table shells are client components.
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- Keep route pages server-first.
|
||||
- Push interaction into feature components and shared UI primitives.
|
||||
|
||||
## 9. Form Convention
|
||||
|
||||
Observed form system:
|
||||
|
||||
- Primary form hook is `useAppForm` from `src/components/ui/tanstack-form.tsx`.
|
||||
- Typed field helpers come from `useFormFields<T>()`.
|
||||
- Validation is Zod-based.
|
||||
- Form dialogs/sheets submit through React Query mutations.
|
||||
|
||||
Observed sheet/dialog pattern:
|
||||
|
||||
- `src/features/users/components/user-form-sheet.tsx` is the best current reference.
|
||||
- Forms use `Sheet`, `SheetContent`, `SheetHeader`, `SheetFooter`.
|
||||
- Submit button uses `<Button isLoading={isPending}>`.
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- CRM forms should use `useAppForm`, typed fields, Zod schemas, and mutation-backed submission.
|
||||
- Prefer `Sheet` for create/edit overlays when the current dashboard pattern already uses it.
|
||||
|
||||
## 10. Table Convention
|
||||
|
||||
Observed table pattern:
|
||||
|
||||
- Shared table shell is `src/components/ui/table/data-table.tsx`.
|
||||
- State management helper is `src/hooks/use-data-table.ts`.
|
||||
- URL state is driven by `nuqs`.
|
||||
- Feature table pages prefetch on the server and render table components on the client.
|
||||
|
||||
Key conventions:
|
||||
|
||||
- Manual pagination, sorting, and filtering are expected.
|
||||
- Query string keys are standardized via `src/lib/searchparams.ts`.
|
||||
- Data tables rely on TanStack Table and TanStack Query.
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- CRM list pages should keep the same table state model when the UI is tabular.
|
||||
- If a CRM list is card-based per `Layout.md` Template B, the server data contract can still reuse the same filter, sort, and pagination conventions.
|
||||
|
||||
## 11. API / Server Action Convention
|
||||
|
||||
Observed production boundary:
|
||||
|
||||
- Feature services call `apiClient`.
|
||||
- Route handlers perform auth, access control, and database work.
|
||||
- Current repo prefers route handlers over direct client-to-DB access.
|
||||
|
||||
Observed examples:
|
||||
|
||||
- `src/app/api/products/route.ts`
|
||||
- `src/app/api/products/[id]/route.ts`
|
||||
- `src/app/api/users/route.ts`
|
||||
- `src/app/api/users/[id]/route.ts`
|
||||
- `src/app/api/organizations/route.ts`
|
||||
- `src/app/api/organizations/active/route.ts`
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- CRM production code should follow route handlers plus Drizzle, not in-memory feature services.
|
||||
- Server actions are acceptable only when they fit naturally, but route handlers are the stronger established repo pattern.
|
||||
|
||||
## 12. Drizzle / Database Convention
|
||||
|
||||
Observed schema in `src/db/schema.ts`:
|
||||
|
||||
- `users`
|
||||
- `organizations`
|
||||
- `memberships`
|
||||
- `products`
|
||||
|
||||
Current conventions:
|
||||
|
||||
- IDs for app-owned auth entities are `text` primary keys.
|
||||
- Organization-scoped business entities include `organizationId`.
|
||||
- Timestamps are timezone-aware and default to `now`.
|
||||
- `users.activeOrganizationId` stores active workspace context.
|
||||
- `memberships.permissions` is an array column.
|
||||
|
||||
Freeze recommendation for CRM vNext:
|
||||
|
||||
- Every CRM entity that belongs to a tenant must carry `organizationId`.
|
||||
- `branchId` should be an additional column where needed for business scoping, not a replacement for `organizationId`.
|
||||
- Audit and workflow entities should reference organization first, then branch if applicable.
|
||||
|
||||
## 13. Auth Convention
|
||||
|
||||
Current auth baseline is real and already committed:
|
||||
|
||||
- Auth.js config lives in `src/auth.ts`.
|
||||
- Credentials provider checks `users.password_hash`.
|
||||
- Session is enriched with organizations, active organization, membership role, business role, and active permissions.
|
||||
- `src/proxy.ts` protects dashboard and protected API routes.
|
||||
|
||||
Preferred server helpers:
|
||||
|
||||
- `requireSession()`
|
||||
- `requireSystemRole()`
|
||||
- `requireOrganizationAccess()`
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- All new CRM protected routes should use shared helpers from `src/lib/auth/session.ts`.
|
||||
- Do not introduce Clerk or alternate auth paths.
|
||||
|
||||
## 14. Organization / Membership / Permission Convention
|
||||
|
||||
Current app-owned model:
|
||||
|
||||
- `users.systemRole`: global role such as `super_admin | user`
|
||||
- `memberships.role`: organization role such as `admin | user`
|
||||
- `memberships.businessRole`: business/department role such as `it_admin`, `helpdesk`, `auditor`
|
||||
- `memberships.permissions`: fine-grained permissions array
|
||||
|
||||
Relevant code:
|
||||
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/lib/auth/session.ts`
|
||||
- `src/config/nav-config.ts`
|
||||
- `src/hooks/use-nav.ts`
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- CRM authorization must be organization-aware first.
|
||||
- Membership role and explicit permissions should gate organization-scoped actions.
|
||||
- Business role can layer domain-specific behavior but should not replace organization membership checks.
|
||||
|
||||
## 15. Branch Scope Convention
|
||||
|
||||
Task A explicitly requires organization-first scoping. The current repo supports that direction.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Existing real entities use `organizationId` as the tenant boundary.
|
||||
- Active context switching works at the organization level via `users.activeOrganizationId` and `/api/organizations/active`.
|
||||
- CRM mock services use `branchId` inside domain records such as customers, enquiries, quotations, and approvals, which implies branch is a nested business scope.
|
||||
|
||||
Frozen convention:
|
||||
|
||||
- `organizationId` = tenant / workspace / company group scope
|
||||
- `membership` = user access inside organization
|
||||
- `branchId` = business branch / operational document scope inside organization
|
||||
|
||||
Do not design CRM around branch-first tenancy.
|
||||
|
||||
## 16. UI / Design Token Convention
|
||||
|
||||
Observed visual/token system:
|
||||
|
||||
- Theme system lives under `src/components/themes/**`.
|
||||
- Theme CSS entrypoint is `src/styles/theme.css`.
|
||||
- Default theme is `mongodb` from `src/components/themes/theme.config.ts`.
|
||||
- Multiple named themes already exist; this repo already has a tokenized visual system.
|
||||
|
||||
Observed dashboard tone:
|
||||
|
||||
- Sidebar-based admin shell
|
||||
- Sticky translucent header
|
||||
- Theme switching and documentation sidebar
|
||||
- shadcn cards, sheets, dialogs, tables, and Radix-based interactions
|
||||
|
||||
Freeze recommendation:
|
||||
|
||||
- Reuse the existing theme/token system.
|
||||
- Use `Layout.md` for CRM structure, but map it onto current shadcn/tailwind/theme primitives.
|
||||
- Do not create a parallel design system for Task B.
|
||||
|
||||
## 17. CRM vNext Recommended Convention
|
||||
|
||||
Recommended convention freeze for Task B:
|
||||
|
||||
- Routing:
|
||||
- Use `src/app/dashboard/crm/**` for pages.
|
||||
- Add route handlers under `src/app/api/crm/**` or sub-feature namespaces.
|
||||
- Data model:
|
||||
- Every CRM entity stores `organizationId`.
|
||||
- Add `branchId` only as sub-scope where operationally required.
|
||||
- Feature shape:
|
||||
- Follow `types.ts -> service.ts -> queries.ts -> mutations.ts -> components`.
|
||||
- Auth:
|
||||
- Gate all real CRM reads and writes through `requireOrganizationAccess()`.
|
||||
- UI:
|
||||
- Use `PageContainer`.
|
||||
- Use `Layout.md` Template A for detail pages and Template B for list pages.
|
||||
- Reuse shared shadcn primitives, theme system, icons registry, and infobar pattern.
|
||||
- Data fetching:
|
||||
- Server prefetch with `getQueryClient()`
|
||||
- `HydrationBoundary`
|
||||
- client consumption through React Query
|
||||
- Forms:
|
||||
- `useAppForm`
|
||||
- Zod schema
|
||||
- React Query mutations
|
||||
- Tables/lists:
|
||||
- Keep `nuqs` filter/sort/page conventions
|
||||
- Reuse `useDataTable` when the UX is tabular
|
||||
|
||||
## 18. Files That Should Not Be Touched
|
||||
|
||||
For Task B, these should be treated as source-of-truth or stable infrastructure unless there is a deliberate migration step:
|
||||
|
||||
- `Layout.md`
|
||||
- `AGENTS.md`
|
||||
- `src/auth.ts`
|
||||
- `src/lib/auth/session.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/db/schema.ts`
|
||||
- `src/components/ui/**`
|
||||
- `src/components/layout/page-container.tsx`
|
||||
- `src/components/themes/**`
|
||||
- `src/styles/theme.css`
|
||||
|
||||
Files to avoid using as production references:
|
||||
|
||||
- `src/constants/mock-api.ts`
|
||||
- `src/constants/mock-api-users.ts`
|
||||
- `src/features/crm/api/service.ts`
|
||||
- `src/features/crm/api/queries.ts`
|
||||
- other demo/template routes that still depend on mock or placeholder data
|
||||
|
||||
These legacy/demo files do not need immediate deletion, but Task B should not extend them as the long-term production path.
|
||||
|
||||
## 19. Risks / Concerns
|
||||
|
||||
Key risks before Task B:
|
||||
|
||||
- The repo currently mixes migrated app-owned code and demo/template code, so copying patterns from the wrong feature can reintroduce mock architecture.
|
||||
- `src/features/crm/api/service.ts` is still in-memory mock state and currently uses `branchId` across CRM records without a real persisted organization boundary.
|
||||
- Some nav and page access behavior is client-visible UX filtering only; all CRM protection must still be enforced server-side.
|
||||
- `src/proxy.ts` protects broad route classes, but fine-grained CRM permission rules will still need route handler or page-level checks.
|
||||
- Existing skill text is partially stale, so implementation decisions should always be verified against repository code.
|
||||
|
||||
## 20. Task B Implementation Plan
|
||||
|
||||
Recommended path for Task B:
|
||||
|
||||
1. Define CRM Drizzle tables with `organizationId` as the tenant boundary and `branchId` as optional business sub-scope.
|
||||
2. Add shared CRM API contracts under `src/features/crm/api/types.ts` that reflect real route handler payloads.
|
||||
3. Introduce CRM route handlers under `src/app/api/crm/**` and gate them with `requireOrganizationAccess()`.
|
||||
4. Replace `src/features/crm/api/service.ts` mock-backed functions with `apiClient` calls to local route handlers.
|
||||
5. Keep existing CRM route/page structure where possible, but refit the data path to the real app boundary.
|
||||
6. For list pages, preserve current query, pagination, sort, and hydration conventions.
|
||||
7. For detail pages, implement UI against `Layout.md` Template A using existing shared primitives and theme tokens.
|
||||
8. Only after the boundary is real should branch-specific business logic be layered into CRM workflows.
|
||||
|
||||
Final convention freeze:
|
||||
|
||||
- Organization-first
|
||||
- Membership-aware
|
||||
- Branch as sub-scope
|
||||
- Route-handler plus Drizzle
|
||||
- Server-enforced RBAC
|
||||
- `Layout.md` as CRM layout source of truth
|
||||
- `products` and `users` as production architecture reference
|
||||
337
docs/implementation/task-b-template-audit.md
Normal file
337
docs/implementation/task-b-template-audit.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Task B Template Audit
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Task B implemented the shared foundation layer for ALLA OS CRM vNext without introducing CRM business modules such as Customer, Contact, Enquiry, Quotation, Approval, KPI, Report, or PDF generation.
|
||||
|
||||
The foundation now provides:
|
||||
|
||||
- Current user context helpers
|
||||
- Organization context helpers
|
||||
- Permission helpers
|
||||
- Branch scope abstraction
|
||||
- Master options service and API
|
||||
- Document sequence service
|
||||
- Audit log helper
|
||||
|
||||
This work follows the Task A convention freeze:
|
||||
|
||||
- `organizationId` remains the tenant boundary
|
||||
- membership and permissions remain organization-scoped
|
||||
- `branchId` is treated as business sub-scope only
|
||||
- existing Auth.js and shared auth helpers are reused rather than replaced
|
||||
|
||||
## 2. Files Added
|
||||
|
||||
### Documentation
|
||||
|
||||
- `docs/implementation/task-b-template-audit.md`
|
||||
|
||||
### Schema / Migration
|
||||
|
||||
- `drizzle/0001_orange_mandarin.sql`
|
||||
- `drizzle/meta/0001_snapshot.json`
|
||||
|
||||
### Foundation Helpers and Services
|
||||
|
||||
- `src/features/foundation/auth-context/types.ts`
|
||||
- `src/features/foundation/auth-context/service.ts`
|
||||
- `src/features/foundation/organization-context/service.ts`
|
||||
- `src/features/foundation/permission/service.ts`
|
||||
- `src/features/foundation/branch-scope/types.ts`
|
||||
- `src/features/foundation/branch-scope/service.ts`
|
||||
- `src/features/foundation/master-options/types.ts`
|
||||
- `src/features/foundation/master-options/service.ts`
|
||||
- `src/features/foundation/master-options/api/types.ts`
|
||||
- `src/features/foundation/master-options/api/service.ts`
|
||||
- `src/features/foundation/master-options/api/queries.ts`
|
||||
- `src/features/foundation/master-options/api/mutations.ts`
|
||||
- `src/features/foundation/master-options/components/columns.tsx`
|
||||
- `src/features/foundation/master-options/components/master-options-table.tsx`
|
||||
- `src/features/foundation/master-options/components/master-options-listing.tsx`
|
||||
- `src/features/foundation/document-sequence/types.ts`
|
||||
- `src/features/foundation/document-sequence/service.ts`
|
||||
- `src/features/foundation/audit-log/types.ts`
|
||||
- `src/features/foundation/audit-log/service.ts`
|
||||
|
||||
### API Route
|
||||
|
||||
- `src/app/api/foundation/master-options/route.ts`
|
||||
|
||||
## 3. Files Modified
|
||||
|
||||
- `src/db/schema.ts`
|
||||
- `src/app/dashboard/crm/settings/master-options/page.tsx`
|
||||
- `drizzle/meta/_journal.json`
|
||||
- `drizzle/meta/0000_snapshot.json`
|
||||
|
||||
## 4. Current User Context Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/auth-context/service.ts`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `getCurrentUser()`
|
||||
- `requireCurrentUser()`
|
||||
- `getCurrentUserContext()`
|
||||
|
||||
Returned context shape:
|
||||
|
||||
- `user`
|
||||
- `activeOrganization`
|
||||
- `membership`
|
||||
- `permissions`
|
||||
- `businessRole`
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Reuses `auth()` from `src/auth.ts`
|
||||
- Reuses `requireSession()` from `src/lib/auth/session.ts`
|
||||
- Does not duplicate sign-in or session enrichment logic
|
||||
|
||||
## 5. Organization Context Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/organization-context/service.ts`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `getCurrentOrganization()`
|
||||
- `getActiveOrganizationId()`
|
||||
- `requireOrganizationAccess()`
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Re-exports the existing shared `requireOrganizationAccess()` helper
|
||||
- Reuses current Auth.js active organization state
|
||||
- Preserves `organizationId` as the only tenant boundary
|
||||
|
||||
## 6. Permission Layer Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/permission/service.ts`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `hasPermission(permission)`
|
||||
- `requirePermission(permission)`
|
||||
- `hasBusinessRole(role)`
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Reuses `memberships.permissions`
|
||||
- Reuses `activeBusinessRole`
|
||||
- Gives `super_admin` a bypass through existing session semantics
|
||||
- Does not introduce a second RBAC model
|
||||
|
||||
## 7. Branch Scope Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/branch-scope/service.ts`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `getUserBranches()`
|
||||
- `getActiveBranch()`
|
||||
- `validateBranchAccess()`
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Branches are currently abstracted through master options category `crm_branch`
|
||||
- No CRM branch UI was added
|
||||
- No direct schema migration for a full branch domain model was added
|
||||
- Branch remains a business scope, not a tenant model
|
||||
|
||||
## 8. Master Options Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/master-options/service.ts`
|
||||
- `src/features/foundation/master-options/api/**`
|
||||
- `src/app/api/foundation/master-options/route.ts`
|
||||
|
||||
Schema added in:
|
||||
|
||||
- `src/db/schema.ts` as `msOptions`
|
||||
|
||||
Helpers and services added:
|
||||
|
||||
- `listMasterOptions()`
|
||||
- `getOptionsByCategory()`
|
||||
- `getActiveOptionsByCategory()`
|
||||
|
||||
Supported characteristics:
|
||||
|
||||
- organization-scoped option records
|
||||
- category-based lookup
|
||||
- parent / child option structure
|
||||
- active / inactive state
|
||||
- soft delete via `deletedAt`
|
||||
- reusable query layer for future CRM modules
|
||||
|
||||
Supported category constants:
|
||||
|
||||
- `crm_branch`
|
||||
- `crm_customer_status`
|
||||
- `crm_customer_type`
|
||||
- `crm_enquiry_status`
|
||||
- `crm_quotation_status`
|
||||
- `crm_product_type`
|
||||
- `crm_currency`
|
||||
- `crm_payment_term`
|
||||
- `crm_priority`
|
||||
- `crm_lead_channel`
|
||||
|
||||
## 9. Document Sequence Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/document-sequence/service.ts`
|
||||
|
||||
Schema added in:
|
||||
|
||||
- `src/db/schema.ts` as `documentSequences`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `previewNextDocumentCode()`
|
||||
- `generateNextDocumentCode()`
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- `preview` does not increment
|
||||
- `generate` increments
|
||||
- generation uses transaction plus row lock behavior
|
||||
- code generation stays on the server
|
||||
|
||||
Current default prefixes:
|
||||
|
||||
- `customer -> CUS`
|
||||
- `contact -> CON`
|
||||
- `enquiry -> ENQ`
|
||||
- `quotation -> QT`
|
||||
- `approval -> APV`
|
||||
|
||||
## 10. Audit Log Output
|
||||
|
||||
Implemented in:
|
||||
|
||||
- `src/features/foundation/audit-log/service.ts`
|
||||
|
||||
Schema added in:
|
||||
|
||||
- `src/db/schema.ts` as `trAuditLogs`
|
||||
|
||||
Helpers added:
|
||||
|
||||
- `auditCreate()`
|
||||
- `auditUpdate()`
|
||||
- `auditDelete()`
|
||||
- `auditAction()`
|
||||
|
||||
Supported fields:
|
||||
|
||||
- `beforeData`
|
||||
- `afterData`
|
||||
- `requestId`
|
||||
- `organizationId`
|
||||
- `branchId`
|
||||
- `userId`
|
||||
- `entityType`
|
||||
- `entityId`
|
||||
- `action`
|
||||
|
||||
## 11. UI Output
|
||||
|
||||
Task B only touched the allowed foundation testing page:
|
||||
|
||||
- `src/app/dashboard/crm/settings/master-options/page.tsx`
|
||||
|
||||
What changed:
|
||||
|
||||
- the page no longer points at the old CRM mock reference query
|
||||
- it now uses the new foundation master options listing flow
|
||||
- it keeps the existing dashboard shell and `PageContainer` pattern
|
||||
- it keeps the existing React Query plus DataTable pattern
|
||||
|
||||
What was intentionally not added:
|
||||
|
||||
- no new dashboard shell
|
||||
- no new design system
|
||||
- no CRM business forms or CRUD screens
|
||||
|
||||
## 12. Reuse From Template / Existing Repo
|
||||
|
||||
Reused foundation from the current repo:
|
||||
|
||||
- `src/auth.ts`
|
||||
- `src/lib/auth/session.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/lib/api-client.ts`
|
||||
- `src/lib/query-client.ts`
|
||||
- `src/lib/searchparams.ts`
|
||||
- `PageContainer`
|
||||
- existing DataTable primitives
|
||||
- existing React Query hydration pattern
|
||||
- existing Auth.js session enrichment
|
||||
|
||||
Production reference patterns reused:
|
||||
|
||||
- `src/features/products/**`
|
||||
- `src/features/users/**`
|
||||
|
||||
## 13. Migration Still Needed
|
||||
|
||||
Future work still required after Task B:
|
||||
|
||||
- seed data or admin flows for `ms_options`
|
||||
- seed or setup flows for `document_sequences`
|
||||
- actual CRM entity tables and route handlers
|
||||
- mutation-level integration that calls `auditCreate`, `auditUpdate`, `auditDelete`
|
||||
- branch-to-user assignment model if the product needs something stricter than option-based abstraction
|
||||
- replacement of remaining CRM mock services and pages under `src/features/crm/**`
|
||||
|
||||
## 14. Risks
|
||||
|
||||
Known risks after Task B:
|
||||
|
||||
- the foundation tables exist in schema and migration, but runtime depends on applying the migration
|
||||
- branch scope is still abstraction-only and not yet backed by a dedicated operational model
|
||||
- master options page is migrated, but the rest of CRM still points at mock data
|
||||
- some compile errors still exist in legacy CRM mock files outside the Task B foundation scope
|
||||
|
||||
## 15. Verification Notes
|
||||
|
||||
Completed during Task B:
|
||||
|
||||
- generated Drizzle migration for the new foundation tables
|
||||
- formatted the new and changed files
|
||||
- checked TypeScript compilation
|
||||
|
||||
Compilation note:
|
||||
|
||||
- foundation-layer code added for Task B was cleaned up against its own type issues
|
||||
- project-wide `tsc --noEmit` still reports existing errors in legacy CRM mock files outside the new foundation path
|
||||
|
||||
## 16. Task C Readiness
|
||||
|
||||
Task C can now start on top of this foundation with the following ready:
|
||||
|
||||
- shared user and organization context helpers
|
||||
- permission guard helpers
|
||||
- branch access abstraction
|
||||
- option lookup abstraction for statuses and enumerations
|
||||
- server-side document code generation
|
||||
- shared audit logging helper
|
||||
|
||||
Recommended Task C direction:
|
||||
|
||||
- build `Customer` and `Contact` on top of the new foundation services
|
||||
- keep `organizationId` mandatory on all new CRM entities
|
||||
- use `branchId` only as business sub-scope where needed
|
||||
- wire all mutations through sequence and audit helpers where relevant
|
||||
279
docs/implementation/task-b1-foundation-stabilization.md
Normal file
279
docs/implementation/task-b1-foundation-stabilization.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Task B.1 Foundation Stabilization
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Task B.1 completed two goals:
|
||||
|
||||
1. Foundation Stabilization
|
||||
2. Production Isolation
|
||||
|
||||
The foundation layer from Task B is now safer to use for Task C because:
|
||||
|
||||
- foundation seed data now exists for master options and document sequences
|
||||
- production CRM routes no longer import legacy CRM mock services
|
||||
- legacy CRM demo code has been isolated under dedicated `crm-demo` paths
|
||||
- TypeScript compilation passes after the route and module isolation changes
|
||||
|
||||
No Customer, Contact, Enquiry, Quotation, Approval, KPI, Report, PDF, or Notification production module was added in this task.
|
||||
|
||||
## 2. Foundation Review
|
||||
|
||||
### 2.1 Schema Review
|
||||
|
||||
Reviewed tables introduced in Task B:
|
||||
|
||||
- `ms_options`
|
||||
- `document_sequences`
|
||||
- `tr_audit_logs`
|
||||
|
||||
Current strengths:
|
||||
|
||||
- all three tables consistently use `organization_id`
|
||||
- `ms_options` supports soft delete through `deleted_at`
|
||||
- `ms_options` has a unique constraint on `(organization_id, category, code)`
|
||||
- `document_sequences` has a unique constraint on `(organization_id, document_type, period, branch_id)`
|
||||
- `tr_audit_logs` supports `before_data`, `after_data`, and `request_id`
|
||||
|
||||
Current migration notes:
|
||||
|
||||
- `ms_options` does not yet enforce a foreign key from `parent_id` back to `ms_options.id`
|
||||
- `document_sequences` does not yet enforce a foreign key to a branch model because branch is still abstraction-only
|
||||
- `tr_audit_logs` does not yet enforce foreign keys to `organizations` or `users`
|
||||
- `tr_audit_logs` could benefit from future read-oriented indexes such as `(organization_id, entity_type, entity_id)` once audit queries are introduced
|
||||
|
||||
Decision:
|
||||
|
||||
- no schema change was made in Task B.1 for the above notes because the task explicitly asked not to change schema unless necessary
|
||||
|
||||
### 2.2 Service Review
|
||||
|
||||
Reviewed foundation services:
|
||||
|
||||
- `auth-context`
|
||||
- `organization-context`
|
||||
- `permission`
|
||||
- `branch-scope`
|
||||
- `master-options`
|
||||
- `document-sequence`
|
||||
- `audit-log`
|
||||
|
||||
Review summary:
|
||||
|
||||
- naming is consistent with Task B expectations
|
||||
- return shapes are now stable enough for Task C
|
||||
- organization-first design is preserved across helpers
|
||||
- error handling is explicit in permission, organization, branch, and seed flows
|
||||
|
||||
Implementation adjustments made in Task B.1:
|
||||
|
||||
- `searchParams` was extended so isolated demo routes no longer break type checks
|
||||
- `crm-demo` compile issues were cleaned up without changing its business behavior
|
||||
- production CRM route tree now uses placeholder or foundation-safe pages only
|
||||
|
||||
## 3. Seed Review
|
||||
|
||||
### 3.1 Foundation Seed Added
|
||||
|
||||
Seed script added:
|
||||
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
|
||||
Package scripts updated:
|
||||
|
||||
- `seed:foundation`
|
||||
- `setup:db` now includes `seed:foundation`
|
||||
|
||||
Seed behavior:
|
||||
|
||||
- loads `DATABASE_URL` from `.env.local` or `.env`
|
||||
- reads `organizations` from the database
|
||||
- throws a clear error if no organizations exist
|
||||
- seeds foundation data for every organization found
|
||||
|
||||
### 3.2 Master Options Seeded
|
||||
|
||||
Seeded categories:
|
||||
|
||||
- `crm_branch`
|
||||
- `crm_customer_status`
|
||||
- `crm_customer_type`
|
||||
- `crm_enquiry_status`
|
||||
- `crm_quotation_status`
|
||||
- `crm_product_type`
|
||||
- `crm_currency`
|
||||
- `crm_payment_term`
|
||||
- `crm_priority`
|
||||
- `crm_lead_channel`
|
||||
|
||||
Important notes:
|
||||
|
||||
- seeding is idempotent via `on conflict`
|
||||
- `crm_branch` options are seeded first and reused by document sequence seed
|
||||
- no organization id is hardcoded
|
||||
|
||||
### 3.3 Document Sequence Seeded
|
||||
|
||||
Seeded sequence types:
|
||||
|
||||
- `customer -> CUS`
|
||||
- `contact -> CON`
|
||||
- `enquiry -> ENQ`
|
||||
- `quotation -> QT`
|
||||
- `approval -> APV`
|
||||
|
||||
Behavior:
|
||||
|
||||
- seeded per organization
|
||||
- seeded per seeded branch option
|
||||
- idempotent via `on conflict`
|
||||
- uses the current `YYMM` period
|
||||
- throws clearly if required organization or branch data is unavailable
|
||||
|
||||
## 4. Production Isolation Review
|
||||
|
||||
### 4.1 Production-Ready Path
|
||||
|
||||
Production-safe CRM route path now lives under:
|
||||
|
||||
- `src/app/dashboard/crm/**`
|
||||
|
||||
Current production-safe routes:
|
||||
|
||||
- `/dashboard/crm`
|
||||
- `/dashboard/crm/customers`
|
||||
- `/dashboard/crm/customers/[id]`
|
||||
- `/dashboard/crm/enquiries`
|
||||
- `/dashboard/crm/enquiries/[id]`
|
||||
- `/dashboard/crm/quotations`
|
||||
- `/dashboard/crm/quotations/[id]`
|
||||
- `/dashboard/crm/approvals`
|
||||
- `/dashboard/crm/settings/document-sequences`
|
||||
- `/dashboard/crm/settings/templates`
|
||||
- `/dashboard/crm/settings/master-options`
|
||||
|
||||
Production-safe behavior:
|
||||
|
||||
- no route above imports `@/features/crm-demo/**`
|
||||
- no route above imports mock query/service layers
|
||||
- `master-options` remains connected to the real foundation path
|
||||
- all other CRM production routes are explicit placeholders until Task C and later tasks build real modules
|
||||
|
||||
### 4.2 Legacy Mock Path
|
||||
|
||||
Legacy CRM mock module was moved to:
|
||||
|
||||
- `src/features/crm-demo/**`
|
||||
|
||||
Legacy demo route tree was moved to:
|
||||
|
||||
- `src/app/dashboard/crm-demo/**`
|
||||
|
||||
The old mock flow is still preserved for reference and manual comparison, but it is no longer the production path.
|
||||
|
||||
### 4.3 Route Separation
|
||||
|
||||
Production route:
|
||||
|
||||
- `/dashboard/crm`
|
||||
|
||||
Demo route:
|
||||
|
||||
- `/dashboard/crm-demo`
|
||||
|
||||
Separation achieved:
|
||||
|
||||
- production route tree does not import demo services
|
||||
- demo route tree imports `@/features/crm-demo/**`
|
||||
- demo links inside the moved module now point to `/dashboard/crm-demo/**`
|
||||
|
||||
## 5. Mock Modules Moved
|
||||
|
||||
Moved feature module:
|
||||
|
||||
- `src/features/crm` -> `src/features/crm-demo`
|
||||
|
||||
Moved route tree:
|
||||
|
||||
- `src/app/dashboard/crm` legacy implementation -> `src/app/dashboard/crm-demo`
|
||||
|
||||
Then recreated:
|
||||
|
||||
- `src/app/dashboard/crm/**` as production-safe placeholder/foundation routes
|
||||
|
||||
## 6. Files Added
|
||||
|
||||
### Documentation
|
||||
|
||||
- `docs/implementation/task-b1-foundation-stabilization.md`
|
||||
|
||||
### Foundation Seed
|
||||
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
|
||||
### Production CRM Placeholder Support
|
||||
|
||||
- `src/features/foundation/components/crm-production-placeholder.tsx`
|
||||
|
||||
### Production CRM Routes
|
||||
|
||||
- `src/app/dashboard/crm/page.tsx`
|
||||
- `src/app/dashboard/crm/customers/page.tsx`
|
||||
- `src/app/dashboard/crm/customers/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/quotations/page.tsx`
|
||||
- `src/app/dashboard/crm/quotations/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/approvals/page.tsx`
|
||||
- `src/app/dashboard/crm/settings/document-sequences/page.tsx`
|
||||
- `src/app/dashboard/crm/settings/templates/page.tsx`
|
||||
- `src/app/dashboard/crm/settings/master-options/page.tsx`
|
||||
|
||||
### Demo Route / Feature Path Created By Move
|
||||
|
||||
- `src/app/dashboard/crm-demo/**`
|
||||
- `src/features/crm-demo/**`
|
||||
|
||||
## 7. Files Modified
|
||||
|
||||
- `package.json`
|
||||
- `src/lib/searchparams.ts`
|
||||
- `src/features/crm-demo/api/service.ts`
|
||||
- `src/features/crm-demo/components/customers-table.tsx`
|
||||
- `src/features/crm-demo/components/enquiries-table.tsx`
|
||||
- `src/features/crm-demo/components/quotations-table.tsx`
|
||||
|
||||
## 8. Production Routes Verified
|
||||
|
||||
Verification result:
|
||||
|
||||
- production `/dashboard/crm/**` no longer depends on legacy CRM mock service imports
|
||||
- `master-options` is the only CRM route currently wired to a real foundation-backed data path
|
||||
- all remaining production CRM routes are intentionally placeholders so Task C can start cleanly
|
||||
|
||||
## 9. Remaining Risks
|
||||
|
||||
- production CRM modules still need real tables and route handlers before the placeholders can be replaced
|
||||
- `tr_audit_logs` has no foreign keys or query indexes yet
|
||||
- branch scope is still option-based abstraction and not a dedicated branch domain model
|
||||
- document sequences are seeded by branch option id, so future branch modeling must preserve or migrate that relationship carefully
|
||||
- `crm-demo` still contains legacy mock business logic by design, even though it is isolated from the production path
|
||||
|
||||
## 10. Task C Readiness
|
||||
|
||||
Task C can begin safely now because:
|
||||
|
||||
- production route tree is no longer tied to mock CRM state
|
||||
- foundation seed data can be created repeatably
|
||||
- document sequences can be initialized for real organizations
|
||||
- production CRM paths are reserved for organization-first implementation
|
||||
- TypeScript compilation passes after the isolation work
|
||||
|
||||
Recommended Task C entry point:
|
||||
|
||||
- build `Customer` and `Contact` on top of:
|
||||
- organization context
|
||||
- permission helpers
|
||||
- branch abstraction
|
||||
- master options
|
||||
- document sequence helper
|
||||
- audit log helper
|
||||
105
docs/implementation/task-c-customer-contact.md
Normal file
105
docs/implementation/task-c-customer-contact.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Task C: Customer + Contact Production Module
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `src/app/api/crm/customers/route.ts`
|
||||
- `src/app/api/crm/customers/[id]/route.ts`
|
||||
- `src/app/api/crm/customers/[id]/contacts/route.ts`
|
||||
- `src/app/api/crm/customers/[id]/contacts/[contactId]/route.ts`
|
||||
- `src/features/crm/customers/api/types.ts`
|
||||
- `src/features/crm/customers/api/service.ts`
|
||||
- `src/features/crm/customers/api/queries.ts`
|
||||
- `src/features/crm/customers/api/mutations.ts`
|
||||
- `src/features/crm/customers/schemas/customer.schema.ts`
|
||||
- `src/features/crm/customers/server/service.ts`
|
||||
- `src/features/crm/customers/components/customer-status-badge.tsx`
|
||||
- `src/features/crm/customers/components/customer-form-sheet.tsx`
|
||||
- `src/features/crm/customers/components/contact-form-sheet.tsx`
|
||||
- `src/features/crm/customers/components/customer-cell-action.tsx`
|
||||
- `src/features/crm/customers/components/customer-columns.tsx`
|
||||
- `src/features/crm/customers/components/customers-table.tsx`
|
||||
- `src/features/crm/customers/components/customer-listing.tsx`
|
||||
- `src/features/crm/customers/components/customer-contacts-tab.tsx`
|
||||
- `src/features/crm/customers/components/customer-detail.tsx`
|
||||
- `drizzle/0002_plain_anthem.sql`
|
||||
- `drizzle/meta/0002_snapshot.json`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/app/dashboard/crm/customers/page.tsx`
|
||||
- `src/app/dashboard/crm/customers/[id]/page.tsx`
|
||||
- `src/db/schema.ts`
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `src/features/foundation/audit-log/service.ts`
|
||||
- `src/features/foundation/audit-log/types.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/lib/searchparams.ts`
|
||||
- `drizzle/meta/_journal.json`
|
||||
|
||||
## 3. Schema Added
|
||||
|
||||
- `crm_customers`
|
||||
- organization-scoped customer master with unique `(organization_id, code)`
|
||||
- branch, CRM classification, contact channels, notes, soft-delete, and audit actor columns
|
||||
- `crm_customer_contacts`
|
||||
- organization-scoped child contacts under customer
|
||||
- primary contact flag, soft-delete, and audit actor columns
|
||||
|
||||
## 4. API Routes Added
|
||||
|
||||
- `GET /api/crm/customers`
|
||||
- `POST /api/crm/customers`
|
||||
- `GET /api/crm/customers/[id]`
|
||||
- `PATCH /api/crm/customers/[id]`
|
||||
- `DELETE /api/crm/customers/[id]`
|
||||
- `GET /api/crm/customers/[id]/contacts`
|
||||
- `POST /api/crm/customers/[id]/contacts`
|
||||
- `PATCH /api/crm/customers/[id]/contacts/[contactId]`
|
||||
- `DELETE /api/crm/customers/[id]/contacts/[contactId]`
|
||||
|
||||
## 5. UI Routes Completed
|
||||
|
||||
- `/dashboard/crm/customers`
|
||||
- production list with React Query, nuqs filters, create button, edit/view/delete actions
|
||||
- `/dashboard/crm/customers/[id]`
|
||||
- detail page with Template A style structure
|
||||
- tabs: Overview, Contacts, Activity, Related Documents Placeholder
|
||||
|
||||
## 6. Permissions Used
|
||||
|
||||
- `crm.customer.read`
|
||||
- `crm.customer.create`
|
||||
- `crm.customer.update`
|
||||
- `crm.customer.delete`
|
||||
- `crm.contact.read`
|
||||
- `crm.contact.create`
|
||||
- `crm.contact.update`
|
||||
- `crm.contact.delete`
|
||||
|
||||
Admin defaults now include the CRM customer/contact permissions. Regular users inherit read permissions only unless additional permissions are granted.
|
||||
|
||||
## 7. Audit Integration
|
||||
|
||||
- customer create/update/delete writes to `tr_audit_logs` with `entityType = crm_customer`
|
||||
- contact create/update/delete writes to `tr_audit_logs` with `entityType = crm_customer_contact`
|
||||
- activity tab reads audit log entries for the current customer and resolves actor names from `users`
|
||||
|
||||
## 8. Document Sequence Integration
|
||||
|
||||
- customer create uses `generateNextDocumentCode({ documentType: 'customer' })`
|
||||
- branch-specific sequence is honored when `branchId` is provided
|
||||
- foundation seed now also prepares `crm_customer_group` options for form-driven classification
|
||||
|
||||
## 9. Remaining Risks
|
||||
|
||||
- there are no database foreign keys yet between `crm_customers`, `crm_customer_contacts`, and master option rows; organization filtering is enforced in application code
|
||||
- branch scope currently resolves from seeded `crm_branch` options only; if branch ownership becomes role-sensitive later, `branch-scope` will need stronger membership-aware rules
|
||||
- contact primary uniqueness is enforced in transaction logic, not via a database partial unique index
|
||||
- list/detail UI depends on seeded master options being present for customer status/type/group/channel labels
|
||||
|
||||
## 10. Task D Readiness
|
||||
|
||||
- customer and contact production backbone is ready for enquiry linkage
|
||||
- activity timeline is already in place for future downstream CRM entities
|
||||
- customer detail route now has a stable home for related document tabs
|
||||
- foundation permissions, sequence, branch scope, and audit plumbing are reusable for enquiry and quotation modules
|
||||
@@ -0,0 +1,51 @@
|
||||
# Task C.1: Customer Ownership and Contact Sharing Governance
|
||||
|
||||
## Summary
|
||||
|
||||
Task C.1 completes the missing customer-governance layer by adding:
|
||||
|
||||
- persistent customer owner fields and owner history
|
||||
- persistent contact sharing
|
||||
- owner/share-aware customer and contact visibility
|
||||
- lead owner suggestion during lead/enquiry creation
|
||||
|
||||
## Delivered
|
||||
|
||||
- Added `ownerUserId`, `ownerAssignedAt`, and `ownerAssignedBy` to `crm_customers`
|
||||
- Added `crm_customer_owner_history`
|
||||
- Added `crm_contact_shares`
|
||||
- Added customer owner management route:
|
||||
- `PATCH /api/crm/customers/[id]/owner`
|
||||
- `DELETE /api/crm/customers/[id]/owner`
|
||||
- Added contact sharing routes:
|
||||
- `GET /api/crm/customers/[id]/contacts/[contactId]/shares`
|
||||
- `POST /api/crm/customers/[id]/contacts/[contactId]/shares`
|
||||
- `DELETE /api/crm/customers/[id]/contacts/[contactId]/shares/[shareId]`
|
||||
- Expanded customer list/detail contracts with owner and share metadata
|
||||
- Added customer owner card with history on customer detail
|
||||
- Added contact sharing management UI on customer detail
|
||||
- Added owner columns and owner filter on customer list
|
||||
- Added new permissions:
|
||||
- `crm.customer.owner.read`
|
||||
- `crm.customer.owner.manage`
|
||||
- `crm.contact.share.read`
|
||||
- `crm.contact.share.create`
|
||||
- `crm.contact.share.delete`
|
||||
- `crm.contact.share.manage`
|
||||
- Added lead/enquiry assignee suggestion from selected customer owner
|
||||
- Added audit events for:
|
||||
- `assign_owner`
|
||||
- `change_owner`
|
||||
- `clear_owner`
|
||||
- `share`
|
||||
- `unshare`
|
||||
- `lead_owner_suggestion_used`
|
||||
- `lead_owner_suggestion_overridden`
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit`
|
||||
|
||||
## Remaining limitation
|
||||
|
||||
- Team-scope visibility is still approximate because the production model still lacks an explicit CRM team hierarchy.
|
||||
118
docs/implementation/task-d-enquiry-production.md
Normal file
118
docs/implementation/task-d-enquiry-production.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Task D: Enquiry Production Module
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `src/app/api/crm/enquiries/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/followups/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/followups/[followupId]/route.ts`
|
||||
- `src/features/crm/enquiries/api/types.ts`
|
||||
- `src/features/crm/enquiries/api/service.ts`
|
||||
- `src/features/crm/enquiries/api/queries.ts`
|
||||
- `src/features/crm/enquiries/api/mutations.ts`
|
||||
- `src/features/crm/enquiries/schemas/enquiry.schema.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `src/features/crm/enquiries/components/enquiry-status-badge.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-form-sheet.tsx`
|
||||
- `src/features/crm/enquiries/components/followup-form-sheet.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-cell-action.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-columns.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiries-table.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-listing.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-followups-tab.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-detail.tsx`
|
||||
- `drizzle/0003_blushing_bruce_banner.sql`
|
||||
- `drizzle/meta/0003_snapshot.json`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/customers/[id]/page.tsx`
|
||||
- `src/features/crm/customers/api/types.ts`
|
||||
- `src/features/crm/customers/components/customer-detail.tsx`
|
||||
- `src/db/schema.ts`
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/lib/searchparams.ts`
|
||||
- `drizzle/meta/_journal.json`
|
||||
|
||||
## 3. Schema Added
|
||||
|
||||
- `crm_enquiries`
|
||||
- organization-scoped sales opportunity master
|
||||
- customer/contact linkage, project context, pricing expectation, probability, hot-project flag, and soft-delete columns
|
||||
- unique `(organization_id, code)` for document-sequenced enquiry codes
|
||||
- `crm_enquiry_followups`
|
||||
- organization-scoped timeline items under enquiry
|
||||
- follow-up type, contact linkage, outcome, next follow-up date, next action, and soft-delete columns
|
||||
|
||||
## 4. API Routes Added
|
||||
|
||||
- `GET /api/crm/enquiries`
|
||||
- `POST /api/crm/enquiries`
|
||||
- `GET /api/crm/enquiries/[id]`
|
||||
- `PATCH /api/crm/enquiries/[id]`
|
||||
- `DELETE /api/crm/enquiries/[id]`
|
||||
- `GET /api/crm/enquiries/[id]/followups`
|
||||
- `POST /api/crm/enquiries/[id]/followups`
|
||||
- `PATCH /api/crm/enquiries/[id]/followups/[followupId]`
|
||||
- `DELETE /api/crm/enquiries/[id]/followups/[followupId]`
|
||||
|
||||
## 5. UI Routes Completed
|
||||
|
||||
- `/dashboard/crm/enquiries`
|
||||
- production list with React Query, nuqs filters, create button, edit/view/delete actions
|
||||
- filters: search, status, product type, priority, branch, customer
|
||||
- `/dashboard/crm/enquiries/[id]`
|
||||
- detail page with tabs: Overview, Follow-ups, Activity, Related Quotations Placeholder
|
||||
- `/dashboard/crm/customers/[id]`
|
||||
- related documents tab now shows real linked enquiries for that customer
|
||||
|
||||
## 6. Permissions Used
|
||||
|
||||
- `crm.enquiry.read`
|
||||
- `crm.enquiry.create`
|
||||
- `crm.enquiry.update`
|
||||
- `crm.enquiry.delete`
|
||||
- `crm.enquiry.followup.read`
|
||||
- `crm.enquiry.followup.create`
|
||||
- `crm.enquiry.followup.update`
|
||||
- `crm.enquiry.followup.delete`
|
||||
|
||||
Admin defaults now include enquiry and follow-up permissions. Regular users inherit read permissions only unless additional permissions are granted.
|
||||
|
||||
## 7. Audit Integration
|
||||
|
||||
- enquiry create/update/delete writes to `tr_audit_logs` with `entityType = crm_enquiry`
|
||||
- follow-up create/update/delete writes to `tr_audit_logs` with `entityType = crm_enquiry_followup`
|
||||
- enquiry detail activity tab merges enquiry audit and follow-up audit entries, then resolves actor names from `users`
|
||||
|
||||
## 8. Document Sequence Integration
|
||||
|
||||
- enquiry create uses `generateNextDocumentCode({ documentType: 'enquiry' })`
|
||||
- branch-specific sequence is honored when `branchId` is provided
|
||||
- foundation seed now includes `crm_followup_type` options so follow-up forms do not hardcode those values
|
||||
|
||||
## 9. Customer/Contact Integration
|
||||
|
||||
- enquiry create/update validates that customer belongs to the active organization
|
||||
- enquiry contact must belong to the same organization and selected customer
|
||||
- follow-up contact must belong to the same organization and the enquiry's customer
|
||||
- enquiry form customer dropdown is sourced from production customer data
|
||||
- contact dropdown is filtered by selected customer
|
||||
- customer detail route now shows related enquiries in the related-documents tab
|
||||
|
||||
## 10. Remaining Risks
|
||||
|
||||
- there are still no database foreign keys between enquiry tables and customer/contact tables; organization scoping is enforced in application logic
|
||||
- form date fields currently use `YYYY-MM-DD` text inputs because the project field wrapper does not expose a native date mode
|
||||
- customer detail shows linked enquiries, but there is not yet a dedicated customer-side enquiry table with pagination
|
||||
- follow-up activity relies on audit payload contents for relation tracing, not a dedicated combined activity view
|
||||
|
||||
## 11. Task E Readiness
|
||||
|
||||
- enquiry production backbone is ready for quotation creation and linkage
|
||||
- customer, contact, and enquiry lifecycles now share the same foundation patterns: org scope, branch validation, master options, document sequence, and audit logging
|
||||
- enquiry detail already has a stable placeholder tab for quotation integration
|
||||
- follow-up timeline and customer-related enquiry surfacing are in place for downstream sales workflow expansion
|
||||
@@ -0,0 +1,54 @@
|
||||
# Task D.4.6 Implementation Report - 2026-06-30
|
||||
|
||||
## Scope
|
||||
- Fix `POST /api/crm/quotations` returning `400` during document-number generation.
|
||||
- Preserve existing quotation, sequence, branch, organization, and approval rules.
|
||||
- Improve frontend propagation of backend error messages.
|
||||
|
||||
## Root Cause
|
||||
- Quotation create sends `payload.quotationType` as a master-option `id` from category `crm_quotation_type`.
|
||||
- Document-sequence resolution was looking up only `crm_product_type` options.
|
||||
- Because `crm_quotation_type.id` and `crm_product_type.id` are different records, valid quotation-type ids never resolved to a product-type code even though the codes and document-sequence rows already existed.
|
||||
- Result: document-sequence generation failed before quotation insert with message `Missing product type code configuration for quotation type.`
|
||||
|
||||
## Implementation
|
||||
- Added shared resolver [product-type-resolver.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/product-type-resolver.ts) to normalize and resolve:
|
||||
- product-type option ids
|
||||
- product-type codes
|
||||
- document-sequence aliases such as `dock_door` and `solar`
|
||||
- quotation-type ids/codes via reference-option translation into product-type codes
|
||||
- Updated [service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/service.ts) so quotation document-sequence generation now resolves:
|
||||
- `crm_quotation_type` option -> code
|
||||
- code -> matching `crm_product_type`
|
||||
- product type -> document sequence / prefix
|
||||
- Added internal diagnostic details to failed document-sequence audit payloads:
|
||||
- requested product type
|
||||
- normalized requested value
|
||||
- available product-type ids/codes
|
||||
- available quotation-type ids/codes
|
||||
- Updated [api-client.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/api-client.ts) to surface backend `message`/`error` payloads instead of always throwing generic `API error: 400 Bad Request`.
|
||||
|
||||
## Tests
|
||||
- Added [product-type-resolver.test.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-sequence/product-type-resolver.test.ts) covering:
|
||||
- quotation-type id resolution
|
||||
- direct code resolution
|
||||
- alias resolution
|
||||
- non-quotation fallback
|
||||
- unresolved input handling
|
||||
|
||||
## Verification
|
||||
- `npm exec tsc --noEmit`
|
||||
- `node --import tsx --test src/features/foundation/document-sequence/config.test.ts src/features/foundation/document-sequence/product-type-resolver.test.ts`
|
||||
- Live API verification against local app on `http://localhost:3000` on `2026-06-30` with authenticated UAT admin session:
|
||||
- `crane` -> `CRA2606-002`
|
||||
- `dockdoor` -> `DKA2606-001`
|
||||
- `solarcell` -> `SCA2606-001`
|
||||
- `service` -> `SVA2606-001`
|
||||
|
||||
## Regression Notes
|
||||
- No schema changes
|
||||
- No seed changes
|
||||
- No duplicate `ms_options`
|
||||
- No duplicate `document_sequences`
|
||||
- Existing organization and branch scoping preserved
|
||||
- Existing document prefix format preserved
|
||||
@@ -0,0 +1,144 @@
|
||||
# Task D.1: Enquiry Assignment and CRM Role Stabilization
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `src/app/api/crm/enquiries/[id]/_schemas.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/assign/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/reassign/route.ts`
|
||||
- `src/features/crm/enquiries/components/enquiry-assignment-dialog.tsx`
|
||||
- `drizzle/0008_clean_justin_hammer.sql`
|
||||
- `drizzle/meta/0008_snapshot.json`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/db/schema.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/features/crm/enquiries/api/types.ts`
|
||||
- `src/features/crm/enquiries/api/service.ts`
|
||||
- `src/features/crm/enquiries/api/mutations.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `src/features/crm/enquiries/components/enquiry-columns.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiries-table.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-listing.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-detail.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-cell-action.tsx`
|
||||
- `src/features/users/api/types.ts`
|
||||
- `src/features/users/schemas/user.ts`
|
||||
- `src/features/users/components/user-form-sheet.tsx`
|
||||
- `src/app/api/users/_lib.ts`
|
||||
- `src/app/api/users/route.ts`
|
||||
- `src/app/api/organizations/route.ts`
|
||||
- `src/features/foundation/auth-context/service.ts`
|
||||
|
||||
## 3. Schema Added
|
||||
|
||||
Added assignment fields to `crm_enquiries`:
|
||||
|
||||
- `assigned_to_user_id`
|
||||
- `assigned_at`
|
||||
- `assigned_by`
|
||||
- `assignment_remark`
|
||||
|
||||
Rules implemented:
|
||||
|
||||
- assignee must belong to the same organization
|
||||
- assigner is the current user
|
||||
- latest assignment timestamp is persisted
|
||||
- assignment remark is optional
|
||||
|
||||
## 4. API Routes Added
|
||||
|
||||
- `POST /api/crm/enquiries/[id]/assign`
|
||||
- `POST /api/crm/enquiries/[id]/reassign`
|
||||
|
||||
Payload:
|
||||
|
||||
```ts
|
||||
{
|
||||
assignedToUserId: string;
|
||||
remark?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- requires organization access
|
||||
- enforces `crm.enquiry.assign` or `crm.enquiry.reassign`
|
||||
- validates assignee organization membership
|
||||
- rejects assign on already-assigned enquiry
|
||||
- rejects reassign on unassigned enquiry
|
||||
|
||||
## 5. Permissions Added
|
||||
|
||||
- `crm.enquiry.assign`
|
||||
- `crm.enquiry.reassign`
|
||||
|
||||
CRM business-role set was also stabilized around:
|
||||
|
||||
- `sales_manager`
|
||||
- `sales`
|
||||
- `sales_support`
|
||||
- `department_manager`
|
||||
- `top_manager`
|
||||
|
||||
Legacy IT-CENTER business roles were removed from active runtime paths and defaults.
|
||||
|
||||
## 6. UI Added
|
||||
|
||||
Enquiry detail now supports:
|
||||
|
||||
- `Assign Sales`
|
||||
- `Reassign Sales`
|
||||
- assigned sales metadata display
|
||||
|
||||
Enquiry list now shows:
|
||||
|
||||
- `Assigned Sales` column
|
||||
|
||||
Assignment dialog now includes:
|
||||
|
||||
- `Sales User`
|
||||
- `Remark`
|
||||
|
||||
The sales-user dropdown is populated from organization memberships instead of hardcoded values.
|
||||
|
||||
## 7. Audit Integration
|
||||
|
||||
Assignment writes audit logs under:
|
||||
|
||||
- `entityType = crm_enquiry`
|
||||
|
||||
Actions added:
|
||||
|
||||
- `assign`
|
||||
- `reassign`
|
||||
|
||||
Audit payload shape:
|
||||
|
||||
- `beforeData.oldAssignedToUserId`
|
||||
- `afterData.newAssignedToUserId`
|
||||
- `afterData.remark`
|
||||
|
||||
## 8. Membership / Permission Stabilization
|
||||
|
||||
Workspace membership management now uses CRM-aligned business roles instead of the old template roles.
|
||||
|
||||
Updated areas:
|
||||
|
||||
- user API types and validation
|
||||
- membership defaults
|
||||
- organization-management fallbacks
|
||||
- auth-context business-role fallback
|
||||
|
||||
## 9. Remaining Risks
|
||||
|
||||
- current `sales` access still operates at organization scope; there is not yet a server-enforced “own or assigned only” data policy
|
||||
- old database rows that still carry removed legacy business-role strings may need a one-time data backfill if they exist in a real environment
|
||||
- assignable-user filtering currently accepts organization admins plus sales-oriented roles; if future CRM staffing rules become stricter, that policy should move into a dedicated assignment rule layer
|
||||
|
||||
## 10. Verification
|
||||
|
||||
- `npx tsc --noEmit`
|
||||
- `npx oxlint` on changed Task D.1 files
|
||||
@@ -0,0 +1,39 @@
|
||||
# Task D.2 Billing Customer + Project Parties
|
||||
|
||||
## Scope delivered
|
||||
|
||||
- Kept the main CRM field named `Billing Customer` on enquiry and quotation forms/detail pages.
|
||||
- Added a separate `Project Parties` model for enquiries and aligned quotation related customers to the same business concept.
|
||||
- Added `remark` support on quotation project parties and introduced enquiry project-party persistence.
|
||||
- Switched role sourcing to the shared master-option category `crm_project_party_role`.
|
||||
- Added compatibility mapping so legacy quotation roles such as `owner` and `billing` still render correctly.
|
||||
|
||||
## Data model
|
||||
|
||||
- Added `crm_enquiry_customers` for enquiry project parties.
|
||||
- Added `remark` column to `crm_quotation_customers`.
|
||||
- Generated migration `drizzle/0010_calm_wendell_rand.sql`.
|
||||
|
||||
## Server behavior
|
||||
|
||||
- Enquiry create/update now sync `Project Parties` transactionally.
|
||||
- Quotation create/update now sync `Project Parties` transactionally.
|
||||
- Billing customer is auto-added as a `billing_customer` project party during sync.
|
||||
- Duplicate `customerId + roleCode` combinations are de-duplicated during main form sync and rejected in quotation detail CRUD.
|
||||
- Quotation project-party reads normalize legacy role codes to current shared role options.
|
||||
|
||||
## UI changes
|
||||
|
||||
- Enquiry form now has `Billing Customer` plus a `Project Parties` editor.
|
||||
- Enquiry detail now shows `Billing Customer` and `Project Parties` separately.
|
||||
- Quotation form now has `Billing Customer` plus a `Project Parties` editor.
|
||||
- Quotation detail overview separates `Billing Customer` and `Project Parties`.
|
||||
- Quotation customer tab is relabeled to `Project Parties`, shows role clearly, and supports remark editing.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit` passes.
|
||||
- `npm run lint` still fails because of pre-existing repo issues outside this task, including:
|
||||
- `src/components/ui/input-group.tsx`
|
||||
- `src/features/chat/components/message-composer.tsx`
|
||||
- `src/components/forms/demo-form.tsx`
|
||||
@@ -0,0 +1,76 @@
|
||||
# Task D.2.1: Revenue Attribution and Party Governance
|
||||
|
||||
## Scope delivered
|
||||
|
||||
- Froze the revenue attribution rules for CRM reporting.
|
||||
- Froze project-party reporting authority so dashboard and reports use the same source of truth.
|
||||
- Added reusable server-side helpers for grouped revenue analytics by party role.
|
||||
- Added ADR governance so future reporting changes require an explicit decision trail.
|
||||
|
||||
## Frozen rules
|
||||
|
||||
### Revenue owner
|
||||
|
||||
- `Revenue Owner = End Customer`
|
||||
- Authority: project-party role code `end_customer`
|
||||
- Fallback: if no `end_customer` exists, use `billing_customer`
|
||||
|
||||
### Relationship revenue dimensions
|
||||
|
||||
- `Billing Revenue` = quotation revenue grouped by project-party role `billing_customer`
|
||||
- `Contractor Revenue` = quotation revenue grouped by project-party role `contractor`
|
||||
- `Consultant Revenue` = quotation revenue grouped by project-party role `consultant`
|
||||
|
||||
### Multiple party behavior
|
||||
|
||||
- When multiple parties share the same role on a quotation, each party receives full quotation attribution.
|
||||
- This is intentional CRM relationship analytics behavior.
|
||||
- No proration is applied.
|
||||
|
||||
### Reporting authority
|
||||
|
||||
- Reporting must use project-party relationships, not raw customer references by themselves.
|
||||
- Service logic prefers `crm_quotation_customers`.
|
||||
- If quotation parties are missing, service logic falls back to `crm_enquiry_customers` on the linked enquiry.
|
||||
- Cancelled quotations are excluded by default from revenue attribution.
|
||||
|
||||
## Service layer added
|
||||
|
||||
Added server-only reporting helpers under `src/features/crm/reporting/server/`:
|
||||
|
||||
- `getRevenueByEndCustomer()`
|
||||
- `getRevenueByBillingCustomer()`
|
||||
- `getRevenueByContractor()`
|
||||
- `getRevenueByConsultant()`
|
||||
|
||||
Shared behavior:
|
||||
|
||||
- organization scoped
|
||||
- supports active quotation/report filters
|
||||
- groups revenue by customer
|
||||
- counts quotation coverage per attributed customer
|
||||
- respects end-customer fallback to billing-customer for revenue-owner attribution
|
||||
|
||||
## Governance added
|
||||
|
||||
- Added ADR [0010-revenue-attribution-governance.md](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0010-revenue-attribution-governance.md)
|
||||
- This ADR freezes:
|
||||
- revenue owner rule
|
||||
- project-party reporting rule
|
||||
- fallback rule
|
||||
- multiple-party rule
|
||||
|
||||
## Remaining risks
|
||||
|
||||
- quotations and enquiries still use separate party tables, so analytics currently depends on a precedence rule rather than a unified reporting projection
|
||||
- legacy records without synchronized project-party rows may not attribute revenue until backfill or edit/save normalization happens
|
||||
- historical analytics still reflects current project-party state, not an effective-dated party snapshot
|
||||
|
||||
## Task J readiness
|
||||
|
||||
- revenue attribution rules frozen
|
||||
- revenue owner frozen
|
||||
- project-party reporting rules frozen
|
||||
- KPI dimensions frozen for reporting by end customer, billing customer, contractor, and consultant
|
||||
- reusable service layer added for dashboard/report reuse
|
||||
- ADR added
|
||||
109
docs/implementation/task-d3-lead-enquiry-ownership-refinement.md
Normal file
109
docs/implementation/task-d3-lead-enquiry-ownership-refinement.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Task D.3: Lead / Enquiry Ownership Refinement
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `drizzle/0012_odd_fat_cobra.sql`
|
||||
- `docs/adr/0011-lead-enquiry-ownership-model.md`
|
||||
- `docs/implementation/task-d3-lead-enquiry-ownership-refinement.md`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/db/schema.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/features/users/components/user-form-sheet.tsx`
|
||||
- `src/features/crm/enquiries/api/types.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `src/features/crm/quotations/server/service.ts`
|
||||
- `src/app/api/crm/enquiries/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/assign/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/reassign/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/customers/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/followups/route.ts`
|
||||
- `src/app/api/crm/enquiries/[id]/followups/[followupId]/route.ts`
|
||||
- `src/app/api/crm/dashboard/route.ts`
|
||||
- `src/app/api/crm/dashboard/export/route.ts`
|
||||
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/config/nav-config.ts`
|
||||
- `src/features/crm/enquiries/components/enquiry-form-sheet.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-assignment-dialog.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-columns.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-detail.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-cell-action.tsx`
|
||||
- `src/features/crm/dashboard/api/types.ts`
|
||||
- `src/features/crm/dashboard/server/service.ts`
|
||||
- `src/features/crm/dashboard/components/crm-dashboard.tsx`
|
||||
- `src/features/crm/dashboard/components/dashboard-summary-cards.tsx`
|
||||
- `src/features/crm/dashboard/components/dashboard-funnel.tsx`
|
||||
- `src/features/crm/dashboard/components/dashboard-sales-ranking.tsx`
|
||||
- `src/features/crm/dashboard/components/dashboard-followups.tsx`
|
||||
- `src/features/crm/dashboard/components/dashboard-hot-projects.tsx`
|
||||
|
||||
## 3. Pipeline Changes
|
||||
|
||||
- Added `crm_enquiries.pipeline_stage`
|
||||
- Allowed values:
|
||||
- `lead`
|
||||
- `enquiry`
|
||||
- `closed_won`
|
||||
- `closed_lost`
|
||||
- Backfilled legacy enquiry rows:
|
||||
- assigned rows -> `enquiry`
|
||||
- unassigned rows -> `lead`
|
||||
- closed-lost/cancelled status rows -> `closed_lost`
|
||||
- Assign flow now converts `lead -> enquiry`
|
||||
- Quotation accepted/lost/rejected outcomes can synchronize linked enquiry lifecycle to won/lost stages
|
||||
|
||||
## 4. Ownership Changes
|
||||
|
||||
- Added business role `marketing`
|
||||
- Marketing-created records default to `lead`
|
||||
- Sales-created records default to `enquiry`
|
||||
- `sales` and `sales_support` now use server-enforced own-or-assigned enquiry visibility
|
||||
- managers, admins, and marketing retain broader monitoring visibility
|
||||
|
||||
## 5. Visibility Rules Added
|
||||
|
||||
- Marketing can read CRM lead/enquiry monitoring surfaces
|
||||
- Marketing cannot see quotation pricing or approval analytics
|
||||
- Enquiry detail hides related quotation links when the user lacks quotation read access
|
||||
- CRM dashboard hides quotation, revenue, hot-enquiry, sales-ranking, and approval sections when the user lacks those permissions
|
||||
|
||||
## 6. KPI Changes
|
||||
|
||||
- Dashboard summary now uses `pipelineStage` for:
|
||||
- lead count
|
||||
- enquiry count
|
||||
- won count
|
||||
- lost count
|
||||
- User-facing KPI language changed from `Opportunity` to `Enquiry`
|
||||
- Funnel wording now follows lead -> enquiry -> quotation flow
|
||||
- Follow-up and ownership labels now use `Enquiry Owner`
|
||||
|
||||
## 7. ADR Added
|
||||
|
||||
- Added `docs/adr/0011-lead-enquiry-ownership-model.md`
|
||||
- This freezes:
|
||||
- lead ownership
|
||||
- enquiry ownership
|
||||
- assignment conversion behavior
|
||||
- marketing visibility restriction boundaries
|
||||
- won/lost lifecycle naming
|
||||
|
||||
## 8. Migration Notes
|
||||
|
||||
- Apply `drizzle/0012_odd_fat_cobra.sql`
|
||||
- Existing `crm_enquiries` rows are backfilled in-place; no new lead table is introduced
|
||||
- Existing forms and APIs remain shared across lead and enquiry flows
|
||||
- Existing quotation routes remain intact; only linked enquiry lifecycle sync was added
|
||||
|
||||
## 9. Remaining Risks
|
||||
|
||||
- `closed_won` still has no dedicated timestamp field; lifecycle timing remains coarse
|
||||
- dashboard/export visibility now follows permission boundaries, but any future custom report endpoints must preserve the same commercial-data guardrails
|
||||
- quotation-to-enquiry lifecycle sync currently depends on quotation status transitions and does not yet model reopen scenarios explicitly
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit`
|
||||
@@ -0,0 +1,72 @@
|
||||
# Task D3.1: Leads / Enquiries Navigation Separation
|
||||
|
||||
## Files Added
|
||||
|
||||
- `src/app/dashboard/crm/leads/page.tsx`
|
||||
- `src/app/dashboard/crm/leads/[id]/page.tsx`
|
||||
- `docs/implementation/task-d31-leads-enquiries-navigation-separation.md`
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `src/config/nav-config.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/app/api/crm/enquiries/route.ts`
|
||||
- `src/features/crm/dashboard/components/dashboard-summary-cards.tsx`
|
||||
- `src/features/crm/enquiries/api/types.ts`
|
||||
- `src/features/crm/enquiries/api/service.ts`
|
||||
- `src/features/crm/enquiries/api/mutations.ts`
|
||||
- `src/features/crm/enquiries/components/enquiry-listing.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiries-table.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-columns.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-cell-action.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-form-sheet.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-detail.tsx`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `docs/adr/0011-lead-enquiry-ownership-model.md`
|
||||
|
||||
## Navigation Changes
|
||||
|
||||
- Split the combined CRM workspace into `Leads` and `Enquiries`.
|
||||
- Kept a single shared feature module by reusing `src/features/crm/enquiries/**`.
|
||||
|
||||
## Route Changes
|
||||
|
||||
- Added `/dashboard/crm/leads`
|
||||
- Added `/dashboard/crm/leads/[id]`
|
||||
- Reused the same list/detail backend and scoped each page with `pipelineStage`
|
||||
- Redirected lead detail to enquiry detail when the record has already moved past the `lead` stage
|
||||
|
||||
## Permission Changes
|
||||
|
||||
- Added:
|
||||
- `crm.lead.read`
|
||||
- `crm.lead.create`
|
||||
- `crm.lead.update`
|
||||
- `crm.lead.assign`
|
||||
- `crm.lead.delete`
|
||||
- Mapped `marketing` to lead-centric permissions
|
||||
- Kept `sales` on enquiry-centric permissions
|
||||
- Gave manager roles both lead and enquiry capabilities
|
||||
|
||||
## Query Invalidation Added
|
||||
|
||||
- CRM enquiry mutations now invalidate lead/enquiry list queries, detail queries, follow-up queries, and CRM dashboard KPI queries.
|
||||
- Assigned leads now disappear from the Leads workspace and appear in the Enquiries workspace without manual refresh.
|
||||
|
||||
## Dashboard Changes
|
||||
|
||||
- Lead KPI card links to `/dashboard/crm/leads`
|
||||
- Enquiry KPI card links to `/dashboard/crm/enquiries`
|
||||
- List filtering now accepts `pipelineStage` for workspace drill-down
|
||||
|
||||
## ADR Updates
|
||||
|
||||
- Added a `Navigation Separation` section to ADR 0011
|
||||
- Clarified that lead/enquiry are UX workspaces, not separate persisted entities
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- Shared route handlers still live under the enquiry API namespace by design, so the lead workspace depends on the shared `crm_enquiries` backend contract.
|
||||
- Manager permission breadth may need later refinement if `department_manager` and `top_manager` should diverge.
|
||||
41
docs/implementation/task-d4-won-lost-lifecycle-governance.md
Normal file
41
docs/implementation/task-d4-won-lost-lifecycle-governance.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Task D.4 Implementation Summary
|
||||
|
||||
## Completed
|
||||
|
||||
- extended `crm_enquiries` with official won/lost lifecycle fields
|
||||
- added `crm_enquiry_attachments` for purchase order files
|
||||
- seeded `crm_lost_reason` master data
|
||||
- added permissions:
|
||||
- `crm.enquiry.mark_won`
|
||||
- `crm.enquiry.mark_lost`
|
||||
- `crm.enquiry.reopen`
|
||||
- added outcome service operations for:
|
||||
- mark won
|
||||
- mark lost
|
||||
- reopen lost opportunity
|
||||
- added API routes for outcome transitions and PO attachment upload/view/download
|
||||
- added enquiry detail outcome card with:
|
||||
- open / won / lost state
|
||||
- PO data
|
||||
- lost data
|
||||
- closed by / closed date
|
||||
- mark won / mark lost / reopen actions
|
||||
- PO attachment upload and retrieval
|
||||
- removed quotation-status-driven mutation of enquiry pipeline outcome
|
||||
- migrated dashboard summary/funnel/sales won revenue to enquiry outcome helpers
|
||||
- added reporting helpers:
|
||||
- `getWonRevenue()`
|
||||
- `getLostRevenue()`
|
||||
- `getLostByReason()`
|
||||
- `getLostByCompetitor()`
|
||||
- `getWinRate()`
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm exec tsc --noEmit`
|
||||
|
||||
## Notes
|
||||
|
||||
- marketing users still receive outcome state, but PO amount is redacted server-side unless commercial pricing visibility exists
|
||||
- PO attachment endpoints are also restricted to commercial-data viewers
|
||||
- revenue analytics cards still use existing quotation-attribution views for top-party ranking, but won/lost headline KPI and sales won revenue now follow the D.4 lifecycle rule
|
||||
134
docs/implementation/task-d5-lead-enquiry-domain-separation.md
Normal file
134
docs/implementation/task-d5-lead-enquiry-domain-separation.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Task D.5: Lead / Enquiry Domain Separation
|
||||
|
||||
## Status
|
||||
|
||||
In progress
|
||||
|
||||
## Objective
|
||||
|
||||
Replace the current single-record lead/enquiry persistence model with a split domain model that preserves marketing leads independently from sales enquiries.
|
||||
|
||||
## Review Summary
|
||||
|
||||
Reviewed before implementation:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/standards/task-contract-template.md`
|
||||
- `docs/standards/task-review-checklist.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/adr/0011-lead-enquiry-ownership-model.md`
|
||||
- `docs/adr/0016-won-lost-lifecycle-governance.md`
|
||||
- `docs/implementation/task-d3-lead-enquiry-ownership-refinement.md`
|
||||
- `docs/implementation/task-d31-leads-enquiries-navigation-separation.md`
|
||||
- `docs/implementation/task-d4-won-lost-lifecycle-governance.md`
|
||||
- `docs/implementation/task-k2-pipeline-reports.md`
|
||||
- `docs/security/crm-authorization-boundaries.md`
|
||||
- `src/db/schema.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
|
||||
## Historical Findings
|
||||
|
||||
### Current production truth
|
||||
|
||||
The current production CRM still follows ADR-0011:
|
||||
|
||||
- `crm_enquiries` is the single source of truth
|
||||
- `pipeline_stage` distinguishes `lead`, `enquiry`, `closed_won`, `closed_lost`
|
||||
- assigning a lead mutates the same record into an enquiry
|
||||
- lead workspace and enquiry workspace are only UX separation
|
||||
|
||||
### Why D.5 is different
|
||||
|
||||
Task D.5 explicitly requires:
|
||||
|
||||
- dedicated `crm_leads`
|
||||
- `crm_enquiries.lead_id`
|
||||
- `1 Lead -> N Enquiries`
|
||||
- direct sales enquiries with nullable `lead_id`
|
||||
- dashboard/report split by entity rather than `pipeline_stage`
|
||||
|
||||
This is not an incremental extension of ADR-0011. It is a domain replacement.
|
||||
|
||||
## Governance Decision
|
||||
|
||||
Because Task D.5 conflicts with an accepted ADR, the first implementation step is to add a replacement ADR:
|
||||
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
|
||||
This task should not continue with schema or API changes while ADR-0011 remains the only source of truth for this area.
|
||||
|
||||
## Reuse Constraints
|
||||
|
||||
The split-domain refactor must still reuse existing foundations rather than rebuilding CRM from scratch:
|
||||
|
||||
- Audit Foundation for migration and lifecycle actions
|
||||
- Master Data Foundation for lead awareness/status/follow-up/lost-reason options
|
||||
- Document Sequence Foundation for lead and enquiry code generation
|
||||
- Authorization Foundation for visibility and scope rules
|
||||
- Dashboard Foundation for KPI composition
|
||||
- Reporting Foundation for dataset and export contracts
|
||||
- Won / Lost Governance Foundation for enquiry outcome transitions
|
||||
|
||||
## Phase Plan
|
||||
|
||||
### Phase 0
|
||||
|
||||
- freeze ADR-0018
|
||||
- document the conflict with ADR-0011
|
||||
- define migration boundaries
|
||||
|
||||
### Phase 1
|
||||
|
||||
- add `crm_leads`
|
||||
- add `crm_enquiries.lead_id`
|
||||
- add lead master option categories
|
||||
- add lead document sequence type
|
||||
|
||||
### Phase 2
|
||||
|
||||
- create dedicated lead services and `/api/crm/leads/**`
|
||||
- keep enquiry services focused on sales execution
|
||||
- migrate assignment flow from `pipeline_stage` mutation to `lead -> create enquiry`
|
||||
|
||||
### Phase 3
|
||||
|
||||
- update dashboard datasets to separate lead/enquiry sources
|
||||
- update report datasets and exports to separate lead/enquiry sources
|
||||
- preserve quotation, approval, PDF, and pricing foundations
|
||||
|
||||
### Phase 4
|
||||
|
||||
- migrate legacy rows from `crm_enquiries.pipeline_stage`
|
||||
- preserve follow-ups, project-party relations, attachments, quotations, and audit history
|
||||
- remove single-record assumptions from surviving UI and server code
|
||||
|
||||
## Initial Risks
|
||||
|
||||
- this task affects schema, services, APIs, dashboard, reports, permissions, and migration logic at once
|
||||
- existing lead reports and dashboard KPIs currently depend on `pipeline_stage`
|
||||
- current enquiry services bundle lead creation defaults, assignment conversion, and won/lost logic together
|
||||
- authorization rules for marketing monitoring are currently described against the shared enquiry table
|
||||
|
||||
## Deliverables Started
|
||||
|
||||
- added `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- added `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
|
||||
## Next Implementation Slice
|
||||
|
||||
The next safe code slice is:
|
||||
|
||||
1. add schema support for `crm_leads` and `crm_enquiries.lead_id`
|
||||
2. seed new lead option categories and document sequence configuration
|
||||
3. keep existing enquiry routes live while introducing lead-specific route contracts
|
||||
|
||||
## Verification Target
|
||||
|
||||
When code phases begin, verification must include:
|
||||
|
||||
- `npm exec tsc --noEmit`
|
||||
- migration validation for existing `crm_enquiries`
|
||||
- direct sales enquiry creation with `leadId = null`
|
||||
- lead assignment creating enquiries without deleting the lead
|
||||
- outcome synchronization from enquiry results back to lead outcome
|
||||
140
docs/implementation/task-d51-lead-foundation-stabilization.md
Normal file
140
docs/implementation/task-d51-lead-foundation-stabilization.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Task D.5.1: Lead Foundation Stabilization & Output Completion
|
||||
|
||||
## Objective
|
||||
|
||||
Audit the existing D.5 phase-1 lead foundation work, avoid duplicate schema creation, complete missing registration outputs, and confirm current CRM compatibility before D.5.2 lead API work begins.
|
||||
|
||||
## Review Completed
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
- `src/db/schema.ts`
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `drizzle/0020_lead_foundation_schema.sql`
|
||||
- `drizzle/0021_worried_nebula.sql`
|
||||
- `drizzle/meta/_journal.json`
|
||||
- `src/features/foundation/master-options/types.ts`
|
||||
- `src/features/foundation/document-sequence/service.ts`
|
||||
|
||||
## Existing Foundation
|
||||
|
||||
Verified present in the repository:
|
||||
|
||||
- `crm_leads` exists in [schema.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts)
|
||||
- `crm_enquiries.lead_id` exists in [schema.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts)
|
||||
- ADR-0018 exists at [0018-lead-enquiry-domain-separation.md](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0018-lead-enquiry-domain-separation.md)
|
||||
- lead seed categories already exist in [foundation.seed.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/seeds/foundation.seed.ts):
|
||||
- `crm_lead_awareness`
|
||||
- `crm_lead_status`
|
||||
- `crm_lead_followup_status`
|
||||
- `crm_lead_lost_reason`
|
||||
- document sequence defaults already include:
|
||||
- `crm_lead`
|
||||
- `crm_enquiry`
|
||||
|
||||
## Missing Foundation Found
|
||||
|
||||
The missing outputs were not schema objects. They were governance and contract gaps:
|
||||
|
||||
- no dedicated Lead Foundation entry in `project-foundations.md`
|
||||
- no Task D.5 or Task D.5.1 registration in `task-catalog.md`
|
||||
- master-option category type registry did not include the new lead option categories, which made the seed data harder to discover and reuse from foundation settings code
|
||||
- no D.5.1 implementation summary existed
|
||||
|
||||
## Partial / Compatibility Findings
|
||||
|
||||
### Migration history duplication risk
|
||||
|
||||
Lead schema work appears twice in migration history:
|
||||
|
||||
- [0020_lead_foundation_schema.sql](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0020_lead_foundation_schema.sql)
|
||||
- [0021_worried_nebula.sql](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0021_worried_nebula.sql)
|
||||
|
||||
Compatibility status:
|
||||
|
||||
- `0020` is idempotent with `IF NOT EXISTS`
|
||||
- `0021` is a later non-idempotent generated migration for the same structural slice
|
||||
- D.5.1 did not create any additional migration, in line with task rules
|
||||
|
||||
This is recorded as a migration-history cleanup risk for future maintenance, not changed by this task.
|
||||
|
||||
### Sequence compatibility
|
||||
|
||||
The foundation now includes both legacy and future-facing enquiry sequence types:
|
||||
|
||||
- legacy active enquiry service still generates `documentType = enquiry`
|
||||
- lead split foundation seed also registers `crm_enquiry`
|
||||
|
||||
That means the seed layer is ready for split-domain work, but production enquiry creation is still compatible with the older enquiry document-type contract until D.5.2+ refactors the service layer.
|
||||
|
||||
### Active production model
|
||||
|
||||
Current CRM production behavior is still primarily driven by the old shared enquiry foundation:
|
||||
|
||||
- lead and enquiry workspace separation still relies on `crm_enquiries.pipeline_stage`
|
||||
- no lead API, lead UI, or lead assignment flow exists yet
|
||||
- dashboard and reports still rely on the pre-split data model
|
||||
|
||||
This is expected and remains in scope for later D.5 phases.
|
||||
|
||||
## Changes Completed In D.5.1
|
||||
|
||||
- added Lead Foundation registration to `docs/standards/project-foundations.md`
|
||||
- added Task D.5 and Task D.5.1 to `docs/standards/task-catalog.md`
|
||||
- extended `CRM_MASTER_OPTION_CATEGORIES` to include the new lead option categories in `src/features/foundation/master-options/types.ts`
|
||||
- added this implementation note
|
||||
|
||||
## No Duplicate Creation Performed
|
||||
|
||||
D.5.1 intentionally did not:
|
||||
|
||||
- recreate `crm_leads`
|
||||
- recreate `crm_enquiries.lead_id`
|
||||
- generate a new migration
|
||||
- rename existing schema objects
|
||||
- create lead APIs or UI
|
||||
|
||||
## Compatibility Verification
|
||||
|
||||
Verification executed:
|
||||
|
||||
- `npm exec tsc --noEmit`
|
||||
|
||||
Result:
|
||||
|
||||
- current Customer, Contact, Enquiry, Quotation, Dashboard, and Reports code continues to typecheck after the D.5.1 stabilization changes
|
||||
|
||||
## Output Summary
|
||||
|
||||
### Existing Foundation
|
||||
|
||||
- `crm_leads`
|
||||
- `crm_enquiries.lead_id`
|
||||
- `ADR-0018`
|
||||
|
||||
### Missing Foundation
|
||||
|
||||
- Lead Foundation registry entry
|
||||
- Task catalog registration for D.5 and D.5.1
|
||||
- master-option category contract coverage for lead option seeds
|
||||
- D.5.1 implementation summary
|
||||
|
||||
### Completed Foundation
|
||||
|
||||
- Lead schema and seed presence verified
|
||||
- lead option categories registered in foundation type contracts
|
||||
- Lead Foundation registered in the standards registry
|
||||
- Task D.5 and D.5.1 registered in task history
|
||||
- compatibility state documented for D.5.2 planning
|
||||
|
||||
## Ready For Next Task
|
||||
|
||||
The repo is now better prepared for `Task D.5.2 Lead API Foundation` because:
|
||||
|
||||
- the foundation state is discoverable
|
||||
- duplicate schema creation was avoided
|
||||
- lead option categories are registered in the shared master-option contract
|
||||
- current compatibility boundaries are explicitly documented
|
||||
@@ -0,0 +1,94 @@
|
||||
# Task D.5.2: Lead Domain Service & API Foundation
|
||||
|
||||
## Objective
|
||||
|
||||
Create the first production Lead service and API slice on top of the D.5.1 lead foundation without changing legacy enquiry behavior.
|
||||
|
||||
## Review Completed
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d51-lead-foundation-stabilization.md`
|
||||
- `src/db/schema.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `src/features/crm/customers/server/service.ts`
|
||||
- `src/features/crm/security/server/service.ts`
|
||||
- `src/features/foundation/audit-log/service.ts`
|
||||
- `src/features/foundation/document-sequence/service.ts`
|
||||
- `src/features/foundation/master-options/service.ts`
|
||||
|
||||
## What Was Added
|
||||
|
||||
- Lead type contracts in [types.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/types.ts)
|
||||
- Production lead service in [service.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/server/service.ts)
|
||||
- Lead request schemas in [lead.schema.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/schemas/lead.schema.ts)
|
||||
- Route handlers:
|
||||
- [route.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/leads/route.ts)
|
||||
- [route.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/leads/[id]/route.ts)
|
||||
- [route.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/leads/[id]/followups/route.ts)
|
||||
|
||||
## Service Behavior
|
||||
|
||||
- `crm_leads` is now the source of truth for lead CRUD
|
||||
- lead code generation reuses `generateNextDocumentCode()` with `documentType = crm_lead`
|
||||
- lead detail returns `suggestedSalesOwnerId` from `crm_customers.owner_user_id` when a linked customer exists
|
||||
- route handlers do not query Drizzle directly
|
||||
- access is enforced in the service layer through resolved CRM access converted into `CrmSecurityContext`
|
||||
|
||||
## Follow-up Persistence Decision
|
||||
|
||||
Current schema has:
|
||||
|
||||
- `crm_leads`
|
||||
- `crm_enquiry_followups`
|
||||
- no `crm_lead_followups`
|
||||
|
||||
Task D.5.2 also prohibits:
|
||||
|
||||
- new tables
|
||||
- new migrations
|
||||
|
||||
Because of that, this slice implements lead follow-ups by reusing the audit foundation as the persistence stream for lead follow-up events:
|
||||
|
||||
- POST `/api/crm/leads/[id]/followups` writes `create_lead_followup`
|
||||
- GET `/api/crm/leads/[id]/followups` reads those audit events back as lead follow-up history
|
||||
- the latest follow-up status is synchronized into `crm_leads.followup_status`
|
||||
|
||||
This keeps D.5.2 inside its no-migration boundary while still making the API operational.
|
||||
|
||||
## Audit Actions
|
||||
|
||||
This task records:
|
||||
|
||||
- `create_lead`
|
||||
- `update_lead`
|
||||
- `delete_lead`
|
||||
- `view_lead`
|
||||
- `create_lead_followup`
|
||||
|
||||
## Intentional Non-Scope Preservation
|
||||
|
||||
This task did not change:
|
||||
|
||||
- enquiry CRUD
|
||||
- enquiry assignment flow
|
||||
- dashboard datasets
|
||||
- reporting datasets
|
||||
- won/lost synchronization
|
||||
- `crm_enquiries.pipeline_stage`
|
||||
- lead UI pages that still point at the legacy enquiry-based workspace
|
||||
|
||||
## Verification
|
||||
|
||||
Planned verification target for this slice:
|
||||
|
||||
- `npm exec tsc --noEmit`
|
||||
|
||||
## Notes For D.5.3+
|
||||
|
||||
- lead UI still needs to migrate away from enquiry-backed components
|
||||
- lead assignment and lead-to-enquiry conversion remain for later D.5 phases
|
||||
- if future phases need richer editable follow-up history than audit-backed event replay, a dedicated ADR-backed persistence decision is still needed
|
||||
@@ -0,0 +1,108 @@
|
||||
# Task D.5.3: Lead Assignment -> Create Enquiry Foundation
|
||||
|
||||
## Objective
|
||||
|
||||
Establish the first production lead-assignment flow that preserves `crm_leads` as the
|
||||
marketing-owned source record while creating or reusing a linked `crm_enquiries` record for the
|
||||
sales execution workspace.
|
||||
|
||||
## Review Completed
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
|
||||
- `docs/security/crm-authorization-boundaries.md`
|
||||
- `src/features/crm/leads/server/service.ts`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `src/features/crm/customers/server/service.ts`
|
||||
- `src/features/foundation/audit-log/service.ts`
|
||||
- `src/features/foundation/document-sequence/service.ts`
|
||||
|
||||
## What Changed
|
||||
|
||||
- Added lead bootstrap and assignment fields to `crm_leads`:
|
||||
- `description`
|
||||
- `product_type`
|
||||
- `priority`
|
||||
- `estimated_value`
|
||||
- `assigned_sales_owner_id`
|
||||
- `assigned_at`
|
||||
- `assigned_by`
|
||||
- `assignment_remark`
|
||||
- Added [assignment.service.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/server/assignment.service.ts)
|
||||
with:
|
||||
- `assignLead()`
|
||||
- internal `createEnquiryFromLead()`
|
||||
- Added [route.ts](C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/leads/[id]/assign/route.ts)
|
||||
for `POST /api/crm/leads/[id]/assign`
|
||||
- Extended lead DTOs and schemas so new lead records can carry the data needed to bootstrap an
|
||||
enquiry
|
||||
- Added `assigned` to seeded `crm_lead_status`
|
||||
- Tightened enquiry-owner validation so assignees must be sales-assignable members, not merely any
|
||||
organization member
|
||||
|
||||
## Assignment Behavior
|
||||
|
||||
- route permission is `crm.lead.assign`
|
||||
- service validates:
|
||||
- lead exists and is active
|
||||
- actor can access the lead through resolved CRM scope
|
||||
- assignee belongs to the same organization and is a sales-assignable member
|
||||
- if an active enquiry already exists for the lead:
|
||||
- no second enquiry is created
|
||||
- the existing enquiry is returned
|
||||
- lead assignment metadata is synchronized
|
||||
- if no active enquiry exists:
|
||||
- enquiry code generation reuses `generateNextDocumentCode()` with `documentType = crm_enquiry`
|
||||
- enquiry is created with `lead_id = crm_leads.id`
|
||||
- enquiry owner compatibility fields use the existing enquiry model:
|
||||
- `assigned_to_user_id`
|
||||
- `assigned_at`
|
||||
- `assigned_by`
|
||||
- `assignment_remark`
|
||||
|
||||
## Lead -> Enquiry Bootstrap Mapping
|
||||
|
||||
When creating the enquiry from a lead, the flow copies:
|
||||
|
||||
- `customerId`
|
||||
- `contactId`
|
||||
- `description`
|
||||
- `projectName`
|
||||
- `projectLocation`
|
||||
- `productType`
|
||||
- `estimatedValue`
|
||||
- `priority`
|
||||
|
||||
The enquiry title is derived from `lead.projectName` and falls back to `Lead <code>` when the
|
||||
project name is empty.
|
||||
|
||||
## Audit Actions
|
||||
|
||||
This task records:
|
||||
|
||||
- `assign_lead` on `crm_lead`
|
||||
- `create_enquiry_from_lead` on `crm_enquiry`
|
||||
|
||||
Captured linkage includes:
|
||||
|
||||
- `leadId`
|
||||
- `enquiryId`
|
||||
- `salesOwnerId`
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
This task intentionally does not change:
|
||||
|
||||
- quotation flows
|
||||
- approval flows
|
||||
- dashboard datasets
|
||||
- reporting datasets
|
||||
- won/lost lifecycle flows
|
||||
- legacy enquiry list/detail APIs outside the stricter assignee validation
|
||||
|
||||
`crm_enquiries.pipeline_stage` remains in place for compatibility, but lead assignment now creates a
|
||||
dedicated enquiry record instead of mutating a lead record in place.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Task D.5.4: Lead / Enquiry Workspace Separation UI Foundation
|
||||
|
||||
## Objective
|
||||
Establish separate CRM workspaces for marketing-owned Leads and sales-owned Enquiries without changing existing quotation, dashboard, reporting, or approval behavior.
|
||||
|
||||
## Review Completed
|
||||
- `AGENTS.md`
|
||||
- `plans/task-d.5.4.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
|
||||
- `docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md`
|
||||
- `docs/security/crm-authorization-boundaries.md`
|
||||
- `src/config/nav-config.ts`
|
||||
- existing enquiry list, detail, form, and assignment UI
|
||||
- existing lead route handlers and lead assignment route
|
||||
|
||||
## What Changed
|
||||
- Replaced legacy lead route aliases with real lead pages:
|
||||
- [page.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/leads/page.tsx)
|
||||
- [page.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/leads/[id]/page.tsx)
|
||||
- Added lead client API/query/mutation stack:
|
||||
- [service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/api/service.ts)
|
||||
- [queries.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/api/queries.ts)
|
||||
- [mutations.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/api/mutations.ts)
|
||||
- Added lead UI foundation:
|
||||
- [lead-list.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-list.tsx)
|
||||
- [leads-table.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/leads-table.tsx)
|
||||
- [lead-columns.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-columns.tsx)
|
||||
- [lead-form.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-form.tsx)
|
||||
- [lead-detail.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-detail.tsx)
|
||||
- [lead-assignment-dialog.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-assignment-dialog.tsx)
|
||||
- [lead-followup-panel.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-followup-panel.tsx)
|
||||
- Extended lead domain read models in:
|
||||
- [types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/types.ts)
|
||||
- [service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/server/service.ts)
|
||||
- Lead detail now surfaces:
|
||||
- linked enquiries
|
||||
- assignment result visibility
|
||||
- audit-safe activity list
|
||||
- customer/contact/assignable-user reference data for forms
|
||||
- Enquiry detail compatibility updated in [page.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/enquiries/[id]/page.tsx):
|
||||
- preserves existing enquiry component contract
|
||||
- loads linked lead server-side when `leadId` exists
|
||||
- shows a read-only `Source Lead` block with open-lead navigation
|
||||
- Enquiry read DTO now includes `leadId` in:
|
||||
- [types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/api/types.ts)
|
||||
- [service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/server/service.ts)
|
||||
|
||||
## APIs Consumed
|
||||
- `GET /api/crm/leads`
|
||||
- `POST /api/crm/leads`
|
||||
- `GET /api/crm/leads/[id]`
|
||||
- `PATCH /api/crm/leads/[id]`
|
||||
- `DELETE /api/crm/leads/[id]`
|
||||
- `POST /api/crm/leads/[id]/followups`
|
||||
- `POST /api/crm/leads/[id]/assign`
|
||||
- existing enquiry detail and follow-up APIs remain unchanged
|
||||
|
||||
## Permission Mapping
|
||||
- Lead list/detail visibility uses:
|
||||
- `crm.lead.read`
|
||||
- Lead create button and create form use:
|
||||
- `crm.lead.create`
|
||||
- Lead edit actions use:
|
||||
- `crm.lead.update`
|
||||
- Lead delete action uses:
|
||||
- `crm.lead.delete`
|
||||
- Lead assignment action uses:
|
||||
- `crm.lead.assign`
|
||||
- Enquiry detail compatibility block does not widen enquiry permissions; it only renders when the user can already open the enquiry page.
|
||||
|
||||
## Compatibility Notes
|
||||
- CRM nav already exposed separate Leads and Enquiries entries, so D.5.4 kept navigation structure and replaced the lead pages behind it.
|
||||
- Lead assignment still goes through the D.5.3 server assignment flow and does not create enquiries client-side.
|
||||
- Existing enquiry workflow, quotation conversion flow, dashboard datasets, and reports were not changed.
|
||||
|
||||
## Known Limitations
|
||||
- Lead form intentionally does not introduce a separate persisted `notes` field because D.5.4 is out of scope for schema changes.
|
||||
- Lead follow-up entry uses the existing audit-backed foundation and remains compatible with the current D.5.2 persistence approach.
|
||||
|
||||
## Verification
|
||||
- `npm exec tsc --noEmit`
|
||||
@@ -0,0 +1,92 @@
|
||||
# Task D.5.5: Opportunity / Sales Workspace Refinement
|
||||
|
||||
## Objective
|
||||
Refine the sales-facing enquiry workspace so lead-origin enquiries are presented as opportunities without renaming the underlying `crm_enquiries` model or changing quotation, dashboard, report, or approval foundations.
|
||||
|
||||
## Review Completed
|
||||
- `AGENTS.md`
|
||||
- `plans/task-d.5.5.md`
|
||||
- `docs/standards/project-foundations.md`
|
||||
- `docs/standards/task-catalog.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
|
||||
- `docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md`
|
||||
- `docs/implementation/task-d54-lead-enquiry-workspace-separation-ui-foundation.md`
|
||||
- `docs/security/crm-authorization-boundaries.md`
|
||||
- existing enquiry list/detail/form/assignment UI
|
||||
- existing quotation reference-data and creation UI
|
||||
|
||||
## What Changed
|
||||
- Updated [enquiries page](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/enquiries/page.tsx) to use sales-facing opportunity wording in page title and description while keeping the same enquiry route.
|
||||
- Refined list-loading and table UI in:
|
||||
- [enquiry-listing.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-listing.tsx)
|
||||
- [enquiries-table.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiries-table.tsx)
|
||||
- [enquiry-columns.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-columns.tsx)
|
||||
- Opportunity list now emphasizes sales execution fields:
|
||||
- opportunity code
|
||||
- source lead indicator
|
||||
- customer
|
||||
- project name
|
||||
- product type
|
||||
- sales owner
|
||||
- status
|
||||
- estimated value
|
||||
- chance percent
|
||||
- expected close date
|
||||
- next follow-up
|
||||
- created date
|
||||
- Updated [opportunity detail page](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/enquiries/[id]/page.tsx) to load:
|
||||
- optional source lead summary
|
||||
- quotation reference data when quotation creation permission exists
|
||||
- Rebuilt [enquiry-detail.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-detail.tsx) around clearer sales sections:
|
||||
- Opportunity Summary
|
||||
- Source Lead
|
||||
- Customer / Contact
|
||||
- Requirement
|
||||
- Sales Qualification
|
||||
- Follow-up Timeline
|
||||
- Linked Quotations
|
||||
- Activity
|
||||
- Quotation Readiness
|
||||
- Added [source-lead-card.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/source-lead-card.tsx) for read-only lead provenance visibility.
|
||||
- Added [quotation-readiness-panel.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/quotation-readiness-panel.tsx) for non-blocking readiness checks.
|
||||
- Extended [types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/api/types.ts) with optional list-only fields for future source lead and follow-up enrichment without breaking existing API consumers.
|
||||
|
||||
## UI Wording Decisions
|
||||
- Route kept as `/dashboard/crm/enquiries`
|
||||
- Page title changed to `CRM Opportunities`
|
||||
- Detail title changed to `Opportunity Detail`
|
||||
- Technical code and API names remain `enquiry` / `crm_enquiries`
|
||||
|
||||
## APIs and Foundations Reused
|
||||
- Existing enquiry APIs remained unchanged:
|
||||
- `GET /api/crm/enquiries`
|
||||
- `GET /api/crm/enquiries/[id]`
|
||||
- existing follow-up and assignment APIs
|
||||
- Existing quotation reference-data foundation reused for the `Create Quotation` action.
|
||||
- Lead detail service reused for source lead summary display.
|
||||
|
||||
## Permission Mapping
|
||||
- Opportunity list/detail still rely on existing enquiry permissions:
|
||||
- `crm.enquiry.read`
|
||||
- `crm.enquiry.update`
|
||||
- `crm.enquiry.assign`
|
||||
- `crm.enquiry.reassign`
|
||||
- `crm.enquiry.followup.create`
|
||||
- `crm.enquiry.followup.update`
|
||||
- `crm.enquiry.followup.delete`
|
||||
- Quotation action is shown only when `crm.quotation.create` is available.
|
||||
- Source lead visibility on the opportunity page does not widen lead permissions; it is only rendered for users who can already access the enquiry page.
|
||||
|
||||
## Compatibility Notes
|
||||
- `crm_enquiries`, `/api/crm/enquiries`, and existing quotation flows were not renamed.
|
||||
- Records without `lead_id` continue to render as direct sales opportunities.
|
||||
- Dashboard, reports, approvals, and quotation services were not changed.
|
||||
|
||||
## Known Limitations
|
||||
- The current `Create Quotation` action opens the existing quotation creation sheet without automatically preselecting the current opportunity.
|
||||
- Source-lead-specific list enrichment is prepared at the UI contract level, but the list still uses lightweight lead linkage signaling rather than a fully joined source-lead dataset.
|
||||
|
||||
## Verification
|
||||
- `npm exec tsc --noEmit`
|
||||
@@ -0,0 +1,60 @@
|
||||
# Task D.5.5.1: Force Rename Enquiry Domain Opportunity
|
||||
|
||||
## Objective
|
||||
Rename the sales execution domain from `Enquiry` to `Opportunity` across active application code for the development-only reset phase of D.5.5.1.
|
||||
|
||||
## Review Completed
|
||||
- `AGENTS.md`
|
||||
- `plans/task-d.5.5.1.md`
|
||||
- `plans/task-d.5.5.md`
|
||||
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||
- `docs/implementation/task-d55-opportunity-sales-workspace-refinement.md`
|
||||
- `docs/security/crm-authorization-boundaries.md`
|
||||
- `src/db/schema.ts`
|
||||
- `src/config/nav-config.ts`
|
||||
- current CRM route and feature structure
|
||||
|
||||
## What Changed
|
||||
- Renamed active CRM feature module from `src/features/crm/enquiries` to:
|
||||
- [src/features/crm/opportunities](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/opportunities)
|
||||
- Renamed API route base from `src/app/api/crm/enquiries` to:
|
||||
- [src/app/api/crm/opportunities](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/opportunities)
|
||||
- Renamed dashboard route base from `src/app/dashboard/crm/enquiries` to:
|
||||
- [src/app/dashboard/crm/opportunities](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/opportunities)
|
||||
- Renamed opportunity-facing component files, including:
|
||||
- `opportunity-detail.tsx`
|
||||
- `opportunities-table.tsx`
|
||||
- `opportunity-form-sheet.tsx`
|
||||
- `opportunity-followups-tab.tsx`
|
||||
- `opportunity-assignment-dialog.tsx`
|
||||
- `opportunity-outcome-card.tsx`
|
||||
- `opportunity-status-badge.tsx`
|
||||
- Renamed schema file:
|
||||
- `opportunity.schema.ts`
|
||||
- Replaced active application imports, symbols, route strings, and permission identifiers in `src/` from `enquiry` terminology to `opportunity` terminology.
|
||||
- Updated active CRM permission namespace usage in code from `crmEnquiry*` to `crmOpportunity*`.
|
||||
- Updated active sales route URLs from `/dashboard/crm/enquiries` to `/dashboard/crm/opportunities`.
|
||||
- Updated active API URLs from `/api/crm/enquiries` to `/api/crm/opportunities`.
|
||||
- Renamed active report routes from `/dashboard/crm/reports/enquiry-aging` and `/api/crm/reports/enquiry-aging` to `/dashboard/crm/reports/opportunity-aging` and `/api/crm/reports/opportunity-aging`.
|
||||
- Updated active governance references in project foundations, UI/UX rules, and CRM access inventory to point at the `opportunity` domain.
|
||||
- Fixed the corrupted Thai branch label in `src/db/seeds/foundation.seed.ts` and normalized touched config/seed files to UTF-8.
|
||||
|
||||
## Key Rename Direction
|
||||
- `Enquiry` -> `Opportunity`
|
||||
- `enquiry` -> `opportunity`
|
||||
- `crm_enquiries` -> `crm_opportunities`
|
||||
- `crm_enquiry` -> `crm_opportunity`
|
||||
- `crm.enquiry.*` -> `crm.opportunity.*`
|
||||
|
||||
## Development Reset Implication
|
||||
This task is implemented under the development-only breaking-change assumption from the task contract:
|
||||
- database reset is expected
|
||||
- migration history compatibility is not preserved
|
||||
- seed/config/document-sequence data must be regenerated around the new opportunity domain
|
||||
|
||||
## Verification
|
||||
- `npm exec tsc --noEmit`
|
||||
|
||||
## Known Follow-up Areas
|
||||
- Historical docs under `docs/**` still contain legacy enquiry references where they describe earlier phases or legacy architecture.
|
||||
- Database reset, migration regeneration, and seed verification still need to be executed explicitly in the development environment before treating the rename as operationally complete.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Task D.5.6 Opportunity Lifecycle / Won-Lost Alignment
|
||||
|
||||
## Summary
|
||||
|
||||
- Added explicit opportunity outcome fields in `crm_opportunities`:
|
||||
- `outcomeStatus`
|
||||
- `closedAt`
|
||||
- `lostDetail`
|
||||
- `cancelReason`
|
||||
- `noQuotationReason`
|
||||
- Preserved legacy compatibility fields:
|
||||
- `pipelineStage`
|
||||
- `closedWonAt`
|
||||
- `closedLostAt`
|
||||
|
||||
## Lifecycle Model
|
||||
|
||||
- Working stage continues to use `crm_opportunities.status` for now.
|
||||
- UI/API now expose `stage` as an alias of `status`.
|
||||
- Final result is tracked with `outcomeStatus`.
|
||||
|
||||
### Closed outcomes
|
||||
|
||||
- `won`
|
||||
- `lost`
|
||||
- `cancelled`
|
||||
- `no_quotation`
|
||||
|
||||
### Reopen behavior
|
||||
|
||||
- Reopen clears final outcome data.
|
||||
- Reopen returns the opportunity to:
|
||||
- `status = quotation_created`
|
||||
- `outcomeStatus = open`
|
||||
- `pipelineStage = opportunity`
|
||||
|
||||
## Master Options
|
||||
|
||||
- Added/seeded:
|
||||
- `crm_opportunity_stage`
|
||||
- `crm_opportunity_outcome_status`
|
||||
- `crm_opportunity_lost_reason`
|
||||
- `crm_opportunity_cancel_reason`
|
||||
- `crm_opportunity_no_quotation_reason`
|
||||
- Kept legacy compatibility categories:
|
||||
- `crm_opportunity_status`
|
||||
- `crm_lost_reason`
|
||||
|
||||
## API Routes
|
||||
|
||||
- `POST /api/crm/opportunities/[id]/mark-won`
|
||||
- `POST /api/crm/opportunities/[id]/mark-lost`
|
||||
- `POST /api/crm/opportunities/[id]/mark-cancelled`
|
||||
- `POST /api/crm/opportunities/[id]/mark-no-quotation`
|
||||
- `POST /api/crm/opportunities/[id]/reopen`
|
||||
|
||||
## Quotation Alignment
|
||||
|
||||
- Quotation creation is blocked when linked opportunity has a closed `outcomeStatus`.
|
||||
- Quotation creation keeps outcome as `open`.
|
||||
- Opportunity stage sync remains compatibility-first and moves to `quotation_created`.
|
||||
|
||||
## Audit Actions
|
||||
|
||||
- `mark_opportunity_won`
|
||||
- `mark_opportunity_lost`
|
||||
- `mark_opportunity_cancelled`
|
||||
- `mark_opportunity_no_quotation`
|
||||
- `reopen_opportunity`
|
||||
|
||||
## Encoding Guard
|
||||
|
||||
- Verified repository text encoding with `npm run verify:encoding`.
|
||||
- Repaired mojibake on CRM dashboard components in the sales ranking, follow-up, and hot-project sections.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Task D.5.7 Opportunity / Quotation Synchronization Foundation
|
||||
|
||||
## Summary
|
||||
|
||||
- Reused `opportunityId` as the active quotation link field.
|
||||
- Added service-side synchronization foundations instead of embedding sync logic in routes.
|
||||
- Kept `Opportunity` as sales pipeline source of truth.
|
||||
- Kept `Quotation` as commercial document source of truth.
|
||||
|
||||
## New Service Foundations
|
||||
|
||||
- `src/features/crm/opportunities/server/quotation-sync.service.ts`
|
||||
- `syncOpportunityFromQuotationCreated()`
|
||||
- `syncOpportunityFromQuotationApproval()`
|
||||
- `syncOpportunityFromQuotationWon()`
|
||||
- `syncOpportunityFromQuotationLost()`
|
||||
- `src/features/crm/quotations/server/opportunity-link.service.ts`
|
||||
- `assertOpportunityLinkIntegrity()`
|
||||
- `assertRevisionOpportunityLinkIntegrity()`
|
||||
- `auditQuotationOpportunityLink()`
|
||||
|
||||
## Sync Trigger Map
|
||||
|
||||
- Quotation created
|
||||
- Opportunity stage moves to `quotation_created`
|
||||
- Outcome stays `open`
|
||||
- Quotation pending approval
|
||||
- Opportunity stage stays `quotation_created`
|
||||
- Quotation approved
|
||||
- Opportunity stage moves to `negotiation`
|
||||
- Quotation sent to customer
|
||||
- Opportunity stage can move to `negotiation`
|
||||
- Quotation won
|
||||
- Opportunity stage becomes `closed`
|
||||
- Opportunity outcome becomes `won`
|
||||
- Quotation lost
|
||||
- Opportunity closes as `lost` only when no other viable quotation remains
|
||||
|
||||
## Guard Rules
|
||||
|
||||
- Quotation cannot link to an opportunity outside the same organization.
|
||||
- Closed opportunities cannot create new quotations.
|
||||
- Quotation customer must match linked opportunity customer.
|
||||
- Quotation product type must match linked opportunity product type.
|
||||
- Revisions keep the same `opportunityId`.
|
||||
|
||||
## Audit Actions
|
||||
|
||||
- `sync_opportunity_from_quotation_created`
|
||||
- `sync_opportunity_from_quotation_approved`
|
||||
- `sync_opportunity_from_quotation_sent`
|
||||
- `sync_opportunity_from_quotation_won`
|
||||
- `sync_opportunity_from_quotation_lost`
|
||||
- `sync_quotation_opportunity_link`
|
||||
|
||||
## Current UI Integration
|
||||
|
||||
- Opportunity detail now shows richer linked quotation context.
|
||||
- Quotation detail linkage foundation remains ready for follow-up UI expansion.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Manual quotation accept / reject operations are not fully exposed in UI yet.
|
||||
- Opportunity detail shows linked quotations, but full quotation outcome highlighting can still be expanded.
|
||||
- Quotation detail linked opportunity card can be extended further in a follow-up pass.
|
||||
153
docs/implementation/task-e-quotation-production.md
Normal file
153
docs/implementation/task-e-quotation-production.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Task E: Quotation Production Module
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `src/app/api/crm/quotations/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/items/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/items/[itemId]/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/customers/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/topics/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/followups/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/attachments/route.ts`
|
||||
- `src/app/api/crm/quotations/[id]/revisions/route.ts`
|
||||
- `src/features/crm/quotations/api/types.ts`
|
||||
- `src/features/crm/quotations/api/service.ts`
|
||||
- `src/features/crm/quotations/api/queries.ts`
|
||||
- `src/features/crm/quotations/api/mutations.ts`
|
||||
- `src/features/crm/quotations/schemas/quotation.schema.ts`
|
||||
- `src/features/crm/quotations/server/service.ts`
|
||||
- `src/features/crm/quotations/components/quotation-status-badge.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-form-sheet.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-cell-action.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-columns.tsx`
|
||||
- `src/features/crm/quotations/components/quotations-table.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-listing.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-detail.tsx`
|
||||
- `drizzle/0004_worthless_ender_wiggin.sql`
|
||||
- `drizzle/meta/0004_snapshot.json`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/app/dashboard/crm/quotations/page.tsx`
|
||||
- `src/app/dashboard/crm/quotations/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||
- `src/app/dashboard/crm/customers/[id]/page.tsx`
|
||||
- `src/features/crm/enquiries/components/enquiry-detail.tsx`
|
||||
- `src/features/crm/customers/components/customer-detail.tsx`
|
||||
- `src/db/schema.ts`
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `src/lib/auth/rbac.ts`
|
||||
- `src/lib/searchparams.ts`
|
||||
- `drizzle/meta/_journal.json`
|
||||
|
||||
## 3. Schema Added
|
||||
|
||||
- `crm_quotations`
|
||||
- organization-scoped quotation master with document code, customer/contact/enquiry linkage, pricing header, status, revision metadata, and soft-delete columns
|
||||
- `crm_quotation_items`
|
||||
- server-calculated line items under quotation
|
||||
- `crm_quotation_customers`
|
||||
- related parties such as owner, consultant, contractor, and billing
|
||||
- `crm_quotation_topics`
|
||||
- quotation document sections such as scope, exclusions, and payment
|
||||
- `crm_quotation_topic_items`
|
||||
- topic bullet items stored under each section
|
||||
- `crm_quotation_followups`
|
||||
- post-send follow-up timeline under quotation
|
||||
- `crm_quotation_attachments`
|
||||
- metadata-only attachment registry for future file-storage integration
|
||||
|
||||
## 4. API Routes Added
|
||||
|
||||
- `GET /api/crm/quotations`
|
||||
- `POST /api/crm/quotations`
|
||||
- `GET /api/crm/quotations/[id]`
|
||||
- `PATCH /api/crm/quotations/[id]`
|
||||
- `DELETE /api/crm/quotations/[id]`
|
||||
- `GET /api/crm/quotations/[id]/items`
|
||||
- `POST /api/crm/quotations/[id]/items`
|
||||
- `PATCH /api/crm/quotations/[id]/items/[itemId]`
|
||||
- `DELETE /api/crm/quotations/[id]/items/[itemId]`
|
||||
- `GET /api/crm/quotations/[id]/customers`
|
||||
- `POST /api/crm/quotations/[id]/customers`
|
||||
- `PATCH /api/crm/quotations/[id]/customers`
|
||||
- `DELETE /api/crm/quotations/[id]/customers`
|
||||
- `GET /api/crm/quotations/[id]/topics`
|
||||
- `POST /api/crm/quotations/[id]/topics`
|
||||
- `PATCH /api/crm/quotations/[id]/topics`
|
||||
- `DELETE /api/crm/quotations/[id]/topics`
|
||||
- `GET /api/crm/quotations/[id]/followups`
|
||||
- `POST /api/crm/quotations/[id]/followups`
|
||||
- `PATCH /api/crm/quotations/[id]/followups`
|
||||
- `DELETE /api/crm/quotations/[id]/followups`
|
||||
- `GET /api/crm/quotations/[id]/attachments`
|
||||
- `POST /api/crm/quotations/[id]/attachments`
|
||||
- `PATCH /api/crm/quotations/[id]/attachments`
|
||||
- `DELETE /api/crm/quotations/[id]/attachments`
|
||||
- `GET /api/crm/quotations/[id]/revisions`
|
||||
- `POST /api/crm/quotations/[id]/revisions`
|
||||
|
||||
## 5. UI Routes Completed
|
||||
|
||||
- `/dashboard/crm/quotations`
|
||||
- production list with React Query, nuqs filters, create button, edit/view/delete actions
|
||||
- filters: search, status, quotation type, branch, customer, enquiry, hot project
|
||||
- `/dashboard/crm/quotations/[id]`
|
||||
- detail page with tabs: Overview, Items, Customers, Topics, Follow-ups, Attachments, Activity, Approval Placeholder, Document Preview Placeholder
|
||||
- revision chain card with create-revision action
|
||||
- `/dashboard/crm/enquiries/[id]`
|
||||
- related quotations tab now shows real quotation links for the enquiry
|
||||
- `/dashboard/crm/customers/[id]`
|
||||
- related documents tab now shows both enquiries and quotations for the customer
|
||||
|
||||
## 6. Permissions Used
|
||||
|
||||
- `crm.quotation.read`
|
||||
- `crm.quotation.create`
|
||||
- `crm.quotation.update`
|
||||
- `crm.quotation.delete`
|
||||
- `crm.quotation.item.manage`
|
||||
- `crm.quotation.customer.manage`
|
||||
- `crm.quotation.topic.manage`
|
||||
- `crm.quotation.followup.manage`
|
||||
- `crm.quotation.attachment.manage`
|
||||
- `crm.quotation.revision.create`
|
||||
|
||||
Admin defaults include the full quotation permission set. Regular users inherit read access only unless their membership permissions are extended.
|
||||
|
||||
## 7. Audit Integration
|
||||
|
||||
- quotation create/update/delete writes audit entries with `entityType = crm_quotation`
|
||||
- item create/update/delete writes audit entries with `entityType = crm_quotation_item`
|
||||
- related customer create/update/delete writes audit entries with `entityType = crm_quotation_customer`
|
||||
- topic create/update/delete writes audit entries with `entityType = crm_quotation_topic`
|
||||
- follow-up create/update/delete writes audit entries with `entityType = crm_quotation_followup`
|
||||
- attachment metadata create/update/delete writes audit entries with `entityType = crm_quotation_attachment`
|
||||
- quotation detail activity tab now surfaces all quotation-side audit mutations together
|
||||
|
||||
## 8. Document Sequence And Totals
|
||||
|
||||
- quotation create uses `generateNextDocumentCode({ documentType: 'quotation' })`
|
||||
- revisions also consume the same document sequence and keep `parentQuotationId` linkage
|
||||
- item totals are calculated on the server
|
||||
- quotation subtotal, tax, and total are refreshed from current line items after item mutations
|
||||
|
||||
## 9. Enquiry And Customer Integration
|
||||
|
||||
- quotation create/update validates customer, contact, enquiry, branch, and salesman membership against the active organization
|
||||
- selecting an enquiry in the quotation form auto-fills customer/contact/project context where available
|
||||
- enquiry detail now links to real quotations instead of the old Task D placeholder
|
||||
- customer detail now links to both related enquiries and related quotations
|
||||
|
||||
## 10. Remaining Risks
|
||||
|
||||
- quotation item `taxRate` is stored per line, but current aggregate total refresh still uses quotation-header tax for the final quote summary rather than a mixed per-line tax rollup
|
||||
- attachment handling is metadata-only; there is still no binary upload/storage pipeline
|
||||
- revision creation copies header, items, customers, and topics, but does not snapshot follow-ups or attachments
|
||||
- there are still no database foreign keys between quotation tables and the linked CRM entities; organization scoping is enforced in application logic
|
||||
|
||||
## 11. Verification
|
||||
|
||||
- `npx tsc --noEmit`
|
||||
- `npm run gen`
|
||||
96
docs/implementation/task-e1-quotation-stabilization.md
Normal file
96
docs/implementation/task-e1-quotation-stabilization.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Task E.1: Quotation Stabilization Before Approval
|
||||
|
||||
## 1. Files Added
|
||||
|
||||
- `docs/adr/0007-quotation-revision-strategy.md`
|
||||
- `docs/adr/0008-attachment-storage-strategy.md`
|
||||
|
||||
## 2. Files Modified
|
||||
|
||||
- `src/db/seeds/foundation.seed.ts`
|
||||
- `src/features/foundation/master-options/types.ts`
|
||||
- `src/features/crm/quotations/api/types.ts`
|
||||
- `src/features/crm/quotations/server/service.ts`
|
||||
- `src/features/crm/quotations/components/quotation-form-sheet.tsx`
|
||||
- `src/features/crm/quotations/components/quotation-detail.tsx`
|
||||
- `src/features/crm/enquiries/server/service.ts`
|
||||
- `docs/implementation/technical-debt.md`
|
||||
|
||||
## 3. Options Seeded
|
||||
|
||||
- `crm_quotation_type`
|
||||
- `crane`
|
||||
- `dockdoor`
|
||||
- `solarcell`
|
||||
- `service`
|
||||
- `other`
|
||||
- `crm_discount_type`
|
||||
- `fixed`
|
||||
- `percentage`
|
||||
- `crm_unit`
|
||||
- `pcs`
|
||||
- `set`
|
||||
- `lot`
|
||||
- `job`
|
||||
- `crm_sent_via`
|
||||
- `email`
|
||||
- `manual`
|
||||
- `system`
|
||||
- `crm_quotation_customer_role`
|
||||
- `owner`
|
||||
- `consultant`
|
||||
- `contractor`
|
||||
- `billing`
|
||||
- `crm_quotation_topic_type`
|
||||
- `scope`
|
||||
- `exclusion`
|
||||
- `payment`
|
||||
|
||||
## 4. Revision Rule Confirmed
|
||||
|
||||
- `draft`: cannot revise
|
||||
- `cancelled`: cannot revise
|
||||
- `accepted`: can revise
|
||||
- `rejected`: can revise
|
||||
- `sent`: can revise
|
||||
- `approved`: can revise
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- revision guard now runs in quotation server service before a new revision is created
|
||||
- revision readiness is based on quotation status master-option code, not on UI-only conditions
|
||||
- new revision rows attempt to start with `revised` status when that option exists
|
||||
|
||||
## 5. Approval Readiness
|
||||
|
||||
- quotation status seed now explicitly supports:
|
||||
- `draft`
|
||||
- `pending_approval`
|
||||
- `approved`
|
||||
- `rejected`
|
||||
- `sent`
|
||||
- `accepted`
|
||||
- `lost`
|
||||
- `cancelled`
|
||||
- `revised`
|
||||
- quotation detail now exposes a placeholder `Submit for approval` action for `draft` and `revised` quotations
|
||||
- no approval table or approval workflow was added in Task E.1
|
||||
|
||||
## 6. Remaining Risks
|
||||
|
||||
- quotation summary still does not support mixed tax rollup from per-line tax strategies
|
||||
- attachments are still metadata-only and not backed by upload/storage plumbing
|
||||
- revision snapshot still excludes follow-ups and attachments
|
||||
- CRM entity relationships still rely on organization-scoped validation instead of database foreign keys
|
||||
- approval workflow is still pending and starts in Task F
|
||||
|
||||
## 7. Task F Readiness
|
||||
|
||||
- quotation forms now read status, quotation type, currency, discount type, unit, sent via, customer role, and topic type from master options instead of hardcoded UI choices where practical
|
||||
- quotation revision eligibility is explicit and server-enforced
|
||||
- approval-ready status vocabulary exists before approval workflow work begins
|
||||
- ADR notes now capture the revision and attachment strategies so Task F can build on stable assumptions
|
||||
|
||||
## 8. Verification
|
||||
|
||||
- `npx tsc --noEmit`
|
||||
45
docs/implementation/task-f-approval-production.md
Normal file
45
docs/implementation/task-f-approval-production.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Task F - Approval Production
|
||||
|
||||
## Summary
|
||||
|
||||
Task F delivered the first production-ready approval workflow on top of the CRM foundation, starting with quotation approval and keeping the service layer generic enough for future enquiry, PR, PO, and document flows.
|
||||
|
||||
## Delivered Scope
|
||||
|
||||
- Added approval persistence tables in [src/db/schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts).
|
||||
- Seeded the `quotation_standard_approval` workflow with sequential `sales_manager -> department_manager -> top_manager` steps in [src/db/seeds/foundation.seed.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/seeds/foundation.seed.ts).
|
||||
- Added generic approval feature APIs in:
|
||||
- [src/features/foundation/approval/types.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/types.ts)
|
||||
- [src/features/foundation/approval/service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/service.ts)
|
||||
- [src/features/foundation/approval/queries.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/queries.ts)
|
||||
- [src/features/foundation/approval/mutations.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/mutations.ts)
|
||||
- [src/features/foundation/approval/server/service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/server/service.ts)
|
||||
- Added production routes:
|
||||
- [src/app/api/crm/approvals/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/approvals/route.ts)
|
||||
- [src/app/api/crm/approvals/[id]/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/approvals/[id]/route.ts)
|
||||
- [src/app/api/crm/approvals/[id]/approve/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/approvals/[id]/approve/route.ts)
|
||||
- [src/app/api/crm/approvals/[id]/reject/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/approvals/[id]/reject/route.ts)
|
||||
- [src/app/api/crm/approvals/[id]/return/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/approvals/[id]/return/route.ts)
|
||||
- [src/app/api/crm/quotations/[id]/submit-approval/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/quotations/[id]/submit-approval/route.ts)
|
||||
- Added production approvals UI:
|
||||
- list page [src/app/dashboard/crm/approvals/page.tsx](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/approvals/page.tsx)
|
||||
- detail page [src/app/dashboard/crm/approvals/[id]/page.tsx](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/approvals/[id]/page.tsx)
|
||||
- reusable approval panels in [src/features/foundation/approval/components/](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/components)
|
||||
- Replaced the quotation approval placeholder with a real approval tab in [src/features/crm/quotations/components/quotation-detail.tsx](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/components/quotation-detail.tsx) and [src/features/crm/quotations/components/quotation-approval-tab.tsx](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/quotations/components/quotation-approval-tab.tsx).
|
||||
- Added approval permissions and business roles in [src/lib/auth/rbac.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts).
|
||||
- Updated side navigation and search-param plumbing in:
|
||||
- [src/config/nav-config.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/config/nav-config.ts)
|
||||
- [src/lib/searchparams.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/searchparams.ts)
|
||||
|
||||
## Behavioral Notes
|
||||
|
||||
- Quotation submission validates draft or revised status, active customer linkage, and at least one quotation item before opening an approval request.
|
||||
- Final approval promotes quotation status to `approved`.
|
||||
- Reject moves quotation status to `rejected`.
|
||||
- Return or cancel sends quotation back to `draft`.
|
||||
- Approval actions are audited under `crm_approval_request` and `crm_approval_action`.
|
||||
|
||||
## Follow-up
|
||||
|
||||
- Run schema generation and migration creation after reviewing the new tables.
|
||||
- Seed or sync membership permission arrays if non-admin approvers need the new approval permissions immediately in existing environments.
|
||||
81
docs/implementation/task-f2-approval-matrix-foundation.md
Normal file
81
docs/implementation/task-f2-approval-matrix-foundation.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Task F.2: Approval Matrix Foundation
|
||||
|
||||
## Summary
|
||||
|
||||
This task adds approval-matrix based workflow resolution for quotation submission so the system no longer relies on a single static workflow code.
|
||||
|
||||
## Delivered
|
||||
|
||||
- Added `crm_approval_matrices` persistence in [src/db/schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts).
|
||||
- Added SQL migration in [drizzle/0002_calm_approval_matrix.sql](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0002_calm_approval_matrix.sql).
|
||||
- Seeded a default quotation matrix in [src/db/seeds/foundation.seed.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/seeds/foundation.seed.ts) to preserve current behavior.
|
||||
- Added matrix validation in [src/features/crm/approval/schemas/approval-matrix.schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/approval/schemas/approval-matrix.schema.ts).
|
||||
- Added matrix CRUD and resolver services in [src/features/crm/approval/server/service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/approval/server/service.ts).
|
||||
- Added settings APIs:
|
||||
- [src/app/api/crm/settings/approval-matrices/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-matrices/route.ts)
|
||||
- [src/app/api/crm/settings/approval-matrices/[id]/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-matrices/[id]/route.ts)
|
||||
- Updated quotation submit approval flow in [src/app/api/crm/quotations/[id]/submit-approval/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/quotations/[id]/submit-approval/route.ts) to resolve workflow by matrix before creating the approval request.
|
||||
- Extended foundation approval submit input in:
|
||||
- [src/features/foundation/approval/types.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/types.ts)
|
||||
- [src/features/foundation/approval/server/service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/server/service.ts)
|
||||
- Added permissions in [src/lib/auth/rbac.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts):
|
||||
- `crm.approval.matrix.read`
|
||||
- `crm.approval.matrix.create`
|
||||
- `crm.approval.matrix.update`
|
||||
- `crm.approval.matrix.delete`
|
||||
|
||||
## Matching Rules
|
||||
|
||||
- Scope filters consider organization, entity type, active state, and soft delete.
|
||||
- `productType`, `branchId`, and `currency` match exact values or `null` wildcard.
|
||||
- `amount` respects `minAmount` and `maxAmount` when those bounds exist.
|
||||
- Specific rules outrank default rules.
|
||||
- Specificity scoring prefers:
|
||||
- product type match
|
||||
- branch match
|
||||
- currency match
|
||||
- amount-range match
|
||||
- Lower `priority` value wins after specificity.
|
||||
- If specificity and priority tie, the newest active rule wins.
|
||||
- If candidates still tie on the same creation timestamp, the resolver throws a deterministic configuration conflict.
|
||||
- If no specific rule matches, the resolver falls back to an active default rule.
|
||||
|
||||
## Submit Approval Integration
|
||||
|
||||
- Quotation submit approval now:
|
||||
1. loads the quotation
|
||||
2. enforces CRM scoped access
|
||||
3. resolves the approval matrix
|
||||
4. submits approval using the resolved `workflowId`
|
||||
5. writes audit data for the resolution
|
||||
- Existing approve, reject, return, and cancel flows remain unchanged.
|
||||
|
||||
## Audit
|
||||
|
||||
- Matrix CRUD uses `entityType = crm_approval_matrix`.
|
||||
- Matrix maintenance actions:
|
||||
- `create_approval_matrix`
|
||||
- `update_approval_matrix`
|
||||
- `delete_approval_matrix`
|
||||
- Quotation submission writes `resolve_approval_matrix` with:
|
||||
- `quotationId`
|
||||
- `approvalRequestId`
|
||||
- `matrixId`
|
||||
- `workflowId`
|
||||
- `matchReason`
|
||||
- `productType`
|
||||
- `branchId`
|
||||
- `amount`
|
||||
- `currency`
|
||||
|
||||
## Seed Behavior
|
||||
|
||||
- Foundation seed creates or refreshes one active default quotation matrix pointing to `quotation_standard_approval`.
|
||||
- This preserves legacy quotation approval behavior until organization-specific rules are added.
|
||||
- Sample non-default matrices were intentionally not seeded because additional workflows such as executive approval are not yet guaranteed in every environment.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- F.2 adds API and service foundations only. No matrix admin UI is included.
|
||||
- Overlapping active rules are allowed. Resolver handles them deterministically, but configuration hygiene still matters.
|
||||
- Matrix branch, product, and currency values currently assume the same ids already stored on quotation records.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Task F.3: Approval Workflow Builder Foundation
|
||||
|
||||
## Summary
|
||||
|
||||
This task upgrades the existing approval workflow settings surface into a safer workflow builder foundation. Admins can now manage workflow metadata and steps through dedicated APIs and a richer settings UI without editing seed data directly.
|
||||
|
||||
## Delivered
|
||||
|
||||
- Extended approval workflow schema in [src/db/schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts):
|
||||
- workflow: `description`, `isSystem`, `createdBy`, `updatedBy`
|
||||
- step: `approvalMode`
|
||||
- Added migration [drizzle/0003_workflow_builder_foundation.sql](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0003_workflow_builder_foundation.sql)
|
||||
- Added builder validation schemas:
|
||||
- [src/features/crm/approval/schemas/approval-workflow.schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/approval/schemas/approval-workflow.schema.ts)
|
||||
- [src/features/crm/approval/schemas/approval-step.schema.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/approval/schemas/approval-step.schema.ts)
|
||||
- Added builder server rules in [src/features/crm/approval/server/workflow-builder.service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/approval/server/workflow-builder.service.ts)
|
||||
- Replaced the settings APIs with builder-aware behavior under:
|
||||
- [src/app/api/crm/settings/approval-workflows/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/clone/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/clone/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/activate/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/activate/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/deactivate/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/deactivate/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/steps/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/steps/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/steps/[stepId]/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/steps/[stepId]/route.ts)
|
||||
- [src/app/api/crm/settings/approval-workflows/[id]/steps/reorder/route.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/approval-workflows/[id]/steps/reorder/route.ts)
|
||||
- Rebuilt workflow builder client contracts and UI in:
|
||||
- [src/features/foundation/approval/types.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/types.ts)
|
||||
- [src/features/foundation/approval/service.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/service.ts)
|
||||
- [src/features/foundation/approval/queries.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/queries.ts)
|
||||
- [src/features/foundation/approval/mutations.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/mutations.ts)
|
||||
- [src/features/foundation/approval/components/approval-workflow-settings.tsx](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/approval/components/approval-workflow-settings.tsx)
|
||||
|
||||
## Workflow Safety Strategy
|
||||
|
||||
F.3 uses the recommended lock strategy from the task:
|
||||
|
||||
- if a workflow has ever been used by any approval request, the workflow becomes locked for metadata edits
|
||||
- locked workflows also block step create, update, delete, and reorder
|
||||
- admins must clone a used workflow before changing its behavior
|
||||
|
||||
This prevents historical approval requests from silently changing meaning after configuration edits.
|
||||
|
||||
## Clone and Activation Rules
|
||||
|
||||
- clone copies workflow metadata and all active steps
|
||||
- cloned workflows start inactive
|
||||
- deactivation is blocked when:
|
||||
- active approval requests still use the workflow
|
||||
- active approval matrices still point to the workflow
|
||||
- deletion is blocked when:
|
||||
- any approval request has ever used the workflow
|
||||
- active approval matrices still point to the workflow
|
||||
|
||||
## Step Builder Rules
|
||||
|
||||
- steps are managed individually instead of replacing the whole step list
|
||||
- create supports inserting at a target `stepNumber`
|
||||
- delete reorders remaining steps to continuous numbering
|
||||
- reorder uses explicit `orderedStepIds`
|
||||
- active workflows must retain at least one required step
|
||||
|
||||
## Approval Mode Support
|
||||
|
||||
Stored step modes:
|
||||
|
||||
- `sequential`
|
||||
- `any_one`
|
||||
- `all_required`
|
||||
|
||||
Current runtime still executes sequentially only. The builder UI marks non-sequential modes as stored for future use instead of pretending they already affect runtime behavior.
|
||||
|
||||
## Permissions Added
|
||||
|
||||
Extended in [src/lib/auth/rbac.ts](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts):
|
||||
|
||||
- `crm.approval.workflow.clone`
|
||||
- `crm.approval.workflow.activate`
|
||||
- `crm.approval.workflow.deactivate`
|
||||
- `crm.approval.workflow.step.manage`
|
||||
|
||||
Existing workflow read/create/update/delete permissions remain in use.
|
||||
|
||||
## Audit Actions
|
||||
|
||||
Builder routes now emit:
|
||||
|
||||
- `create_approval_workflow`
|
||||
- `update_approval_workflow`
|
||||
- `delete_approval_workflow`
|
||||
- `clone_approval_workflow`
|
||||
- `activate_approval_workflow`
|
||||
- `deactivate_approval_workflow`
|
||||
- `create_approval_step`
|
||||
- `update_approval_step`
|
||||
- `delete_approval_step`
|
||||
- `reorder_approval_steps`
|
||||
|
||||
## Matrix Integration Hook
|
||||
|
||||
Workflow list API now accepts optional filters:
|
||||
|
||||
- `entityType`
|
||||
- `activeOnly`
|
||||
|
||||
This keeps Approval Matrix integration future-ready without requiring F.3 to build the matrix UI itself.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- approval runtime still executes sequentially even if a step stores `any_one` or `all_required`
|
||||
- no drag-and-drop builder is included; reorder uses simple up/down actions
|
||||
- no workflow version table exists yet; lock-and-clone is the current compatibility strategy
|
||||
- matrix admin UI is still outside this task scope
|
||||
116
docs/implementation/task-f4-notification-framework-foundation.md
Normal file
116
docs/implementation/task-f4-notification-framework-foundation.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Task F4: Notification Framework Foundation
|
||||
|
||||
## Scope Delivered
|
||||
|
||||
- Added notification foundation schema for events, inbox items, templates, and deliveries.
|
||||
- Added reusable notification services for event publishing, recipient resolution, template rendering, and inbox mutations.
|
||||
- Integrated approval lifecycle notifications for submit, approve-complete, reject, return, and cancel flows.
|
||||
- Replaced the old mock/Zustand notification UI with API-backed React Query notifications in the header bell and notifications page.
|
||||
|
||||
## Schema
|
||||
|
||||
New tables:
|
||||
|
||||
- `app_notification_events`
|
||||
- `app_notifications`
|
||||
- `app_notification_templates`
|
||||
- `app_notification_deliveries`
|
||||
|
||||
Key behavior:
|
||||
|
||||
- organization-scoped rows throughout
|
||||
- `dedupe_key` support on events
|
||||
- inbox rows track `read_at`, `archived_at`, and `status`
|
||||
- delivery rows prepare future multi-channel expansion
|
||||
|
||||
## Event Flow
|
||||
|
||||
Current flow:
|
||||
|
||||
1. Approval service completes its main action.
|
||||
2. Approval service calls `publishNotificationEvent()` with a safe wrapper.
|
||||
3. Notification foundation persists `app_notification_events`.
|
||||
4. Notification foundation resolves recipients and renders the matching template.
|
||||
5. In-app notifications are created in `app_notifications`.
|
||||
6. Header bell and `/dashboard/notifications` read from the inbox APIs.
|
||||
|
||||
Notification failures are intentionally non-blocking for approval actions. Failures are stored on the notification event row and audited.
|
||||
|
||||
## Recipient Resolution
|
||||
|
||||
Implemented resolvers:
|
||||
|
||||
- `explicit_user`
|
||||
- `approval_current_step_approvers`
|
||||
- `approval_requester`
|
||||
|
||||
Current approval approver resolution uses:
|
||||
|
||||
- active CRM role assignments mapped to the current step role code
|
||||
- membership `business_role` as a fallback path
|
||||
|
||||
Recipients are deduplicated and actor exclusion is supported per event rule.
|
||||
|
||||
## Template Syntax
|
||||
|
||||
Supported placeholders use simple token replacement:
|
||||
|
||||
- `{{quotationCode}}`
|
||||
- `{{workflowName}}`
|
||||
- `{{actorName}}`
|
||||
- `{{currentStepRoleName}}`
|
||||
- `{{entityLink}}`
|
||||
- `{{remark}}`
|
||||
|
||||
Rules:
|
||||
|
||||
- missing values render as `-`
|
||||
- plain text only
|
||||
- no code execution
|
||||
|
||||
## Approval Integration
|
||||
|
||||
Integrated events:
|
||||
|
||||
- `approval.requested`
|
||||
- `approval.step.approved` for step-to-step progression
|
||||
- `approval.completed` for final approval completion
|
||||
- `approval.step.rejected`
|
||||
- `approval.returned`
|
||||
- `approval.cancelled`
|
||||
|
||||
Notes:
|
||||
|
||||
- final approval emits `approval.completed` instead of duplicating both requester notifications
|
||||
- notification delivery is intentionally best-effort and does not roll back approvals
|
||||
|
||||
## Inbox APIs
|
||||
|
||||
Added routes:
|
||||
|
||||
- `GET /api/notifications`
|
||||
- `GET /api/notifications/unread-count`
|
||||
- `POST /api/notifications/[id]/read`
|
||||
- `POST /api/notifications/read-all`
|
||||
- `POST /api/notifications/[id]/archive`
|
||||
|
||||
Access model:
|
||||
|
||||
- `notifications.read`
|
||||
- `notifications.update`
|
||||
- `notifications.admin`
|
||||
|
||||
Normal users can operate only on their own notifications because server-side filtering always scopes by `recipient_user_id`.
|
||||
|
||||
## Default Templates
|
||||
|
||||
Foundation seed now upserts default `in_app` templates for all approval events. Seed remains idempotent and organization-scoped.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- no SMTP sending yet
|
||||
- no LINE delivery yet
|
||||
- no webhook delivery yet
|
||||
- no reminder/escalation scheduler yet
|
||||
- no admin template editor UI yet
|
||||
- no real-time push/SSE yet
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user