44 Commits
main ... setup

Author SHA1 Message Date
phaichayon
3bf3405c48 commit 2026-06-25 17:08:49 +07:00
phaichayon
e72bdfd170 commit 2026-06-25 16:53:11 +07:00
phaichayon
5a83040077 task-fix 2026-06-25 16:26:14 +07:00
phaichayon
69279fc341 task-d.5.7 2026-06-25 15:41:59 +07:00
phaichayon
65dd0e2e56 task-d.5.6 2026-06-25 13:09:21 +07:00
phaichayon
c2a74b6764 task-d5.5.1 2026-06-25 12:16:41 +07:00
phaichayon
f2c7156851 task-d.5.5 2026-06-25 08:27:20 +07:00
phaichayon
6093003b70 task-d.5.4 2026-06-25 08:06:53 +07:00
phaichayon
0c28735c90 task-d.5.3 2026-06-24 14:23:15 +07:00
phaichayon
350239b307 task-d.5.2.1 2026-06-24 13:39:25 +07:00
phaichayon
b42a4c0777 task-d.5.1 2026-06-24 13:00:43 +07:00
phaichayon
9c75788ba7 task-d.5
task-d.5.1
2026-06-24 12:28:04 +07:00
phaichayon
c1ecd5ea50 commit 2026-06-23 22:13:08 +07:00
phaichayon
99a4087099 task-k.2 2026-06-22 21:26:43 +07:00
phaichayon
5c28080e9b task-k.1 2026-06-22 20:07:51 +07:00
phaichayon
ffa5de8311 task-d.4 2026-06-22 16:57:02 +07:00
phaichayon
3f28fde39f task-c.1 2026-06-22 15:49:31 +07:00
phaichayon
1b901efc51 task-l.3.1 2026-06-22 14:18:26 +07:00
phaichayon
42f403a7ed task-l.3 2026-06-22 13:36:00 +07:00
phaichayon
827fd13fa7 task-l.2 2026-06-22 11:39:41 +07:00
phaichayon
771ebfc308 task-l.1 2026-06-22 10:59:31 +07:00
phaichayon
b154a8de33 task-l 2026-06-22 10:22:45 +07:00
phaichayon
51d67ef7c2 task-h.5.3 2026-06-19 17:15:28 +07:00
phaichayon
fd01ebf7c7 pdf task-h 2026-06-19 15:09:58 +07:00
phaichayon
a85d24235c task-h.4 2026-06-19 12:30:26 +07:00
phaichayon
721653f692 thai-uxui 2026-06-19 11:11:17 +07:00
phaichayon
c0cc880be2 lead menu
nquiry menu
2026-06-19 10:39:11 +07:00
phaichayon
0650cf6297 hotfix-add subgroup customer 2026-06-18 08:58:01 +07:00
phaichayon
04a886ea26 task-j1 2026-06-17 16:04:19 +07:00
phaichayon
5be6c54272 taks-d.2.1 2026-06-17 14:46:52 +07:00
phaichayon
0a484e0b45 task-i 2026-06-16 17:01:29 +07:00
phaichayon
90ee59d388 task h.1 adn fix permission 2026-06-16 15:13:14 +07:00
phaichayon
9ae41e4f2c task-h.1 complate 2026-06-16 14:25:26 +07:00
phaichayon
edee45375e task-h 2026-06-16 13:42:38 +07:00
phaichayon
56683ee7b9 Hotfix Rule: Force Fresh Data After CRUD Mutation
Task-h not implements
2026-06-16 11:59:41 +07:00
phaichayon
357414c247 task-g 2026-06-16 10:57:41 +07:00
phaichayon
dff22d75b5 task-f 2026-06-16 09:19:17 +07:00
phaichayon
b8f13e36b3 task-e 2026-06-16 08:43:19 +07:00
phaichayon
eae0dee1f2 task-e 2026-06-15 16:34:41 +07:00
phaichayon
1d2406483e task-d complate 2026-06-15 13:46:33 +07:00
phaichayon
8148850fda tsk-C
compleate
2026-06-15 13:20:39 +07:00
phaichayon
b8cd39eaa4 task-b
task-b.1
complere
2026-06-15 11:19:31 +07:00
phaichayon
89b39cad38 husky delete 2026-06-11 16:58:19 +07:00
phaichayon
67545d9295 task-b complate 2026-06-11 16:55:45 +07:00
667 changed files with 159941 additions and 8689 deletions

12
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,8 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.pdf binary

1
.gitignore vendored
View File

@@ -87,6 +87,7 @@ web_modules/
# Next.js build output
.next
out
public/generated/
# Nuxt.js build / generate output
.nuxt

View File

@@ -1 +0,0 @@
npx lint-staged

View File

@@ -1 +0,0 @@
bun run build

165
AGENTS.md
View File

@@ -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
@@ -375,6 +496,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 +632,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.

View File

@@ -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

View File

@@ -0,0 +1,15 @@
{
"audit": "payload-parity",
"overallStatus": "PASS",
"quotationCode": "QT-H5-AUDIT",
"previewHash": "1914aaab07b562d967c63d9f3bdea6d44b583e4aac39ea590d08e49975391675",
"generationHash": "1914aaab07b562d967c63d9f3bdea6d44b583e4aac39ea590d08e49975391675",
"snapshotHash": "1914aaab07b562d967c63d9f3bdea6d44b583e4aac39ea590d08e49975391675",
"mismatches": [],
"comparableFieldCount": 35,
"topicInputKeyCount": 10,
"signatureParity": true,
"priceTableParity": true,
"priceTableRowCount": 1,
"priceTableCurrency": "THB"
}

View File

@@ -0,0 +1,30 @@
{
"audit": "placeholder-integrity",
"overallStatus": "PASS",
"results": [
{
"templateName": "ALLA Quotation Standard",
"organizationId": "2330c067-2737-4443-bd6e-ccb8a6cf856b",
"version": "1.1",
"duplicateFieldNames": [],
"typoFields": [],
"unmappedPlaceholders": [],
"deadMappings": [],
"invalidReservedMappings": [],
"missingReservedFields": [],
"status": "PASS"
},
{
"templateName": "ONVALLA Quotation Standard",
"organizationId": "8cd23975-e199-4ed0-85a9-8cbd01c810d2",
"version": "1.1",
"duplicateFieldNames": [],
"typoFields": [],
"unmappedPlaceholders": [],
"deadMappings": [],
"invalidReservedMappings": [],
"missingReservedFields": [],
"status": "PASS"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"audit": "approved-snapshot-parity",
"overallStatus": "PASS",
"quotationCode": "QT-H5-AUDIT",
"approvedSnapshotAvailable": true,
"approvedTemplateVersionId": "bd6713d5-a731-482f-a5bb-0dea9d766301",
"computedTemplateVersionId": "bd6713d5-a731-482f-a5bb-0dea9d766301",
"inconsistentTemplateKeys": [],
"computedSignatureHash": "39e2c42458c8b08b4299dc3136c9812c311ae7886df91078f4c9ae6e87bdcc00",
"storedSignatureHash": "39e2c42458c8b08b4299dc3136c9812c311ae7886df91078f4c9ae6e87bdcc00",
"approvedArtifactReference": "artifact:a1fd951d-f0b4-4e5b-be88-073596b3829d",
"refreshedFixtureSnapshot": false
}

View File

@@ -0,0 +1,28 @@
{
"generatedAt": "2026-06-19T10:12:54.977Z",
"overallStatus": "PASS",
"quotationCode": "QT-H5-AUDIT",
"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"
}
}

View File

@@ -0,0 +1,27 @@
# PDF Integrity Summary
- Generated At: 2026-06-19T10:12:54.977Z
- Overall Status: PASS
- Quotation Code: QT-H5-AUDIT
## 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

View File

@@ -0,0 +1,9 @@
{
"audit": "template-version-integrity",
"overallStatus": "PASS",
"activeTemplateCount": 2,
"quotationCode": "QT-H5-AUDIT",
"approvedTemplateVersionId": "bd6713d5-a731-482f-a5bb-0dea9d766301",
"approvedTemplateVersionIsActive": true,
"issues": []
}

View File

@@ -0,0 +1,22 @@
{
"audit": "topic-runtime",
"overallStatus": "PASS",
"quotationCode": "QT-H5-AUDIT",
"topicCount": 5,
"topicInputKeys": [
"topic_1_0",
"topic_1_1",
"topic_1_2",
"topic_1_3",
"topic_1_4"
],
"itemTopicInputKeys": [
"item_topic_1_0",
"item_topic_1_1",
"item_topic_1_2",
"item_topic_1_3",
"item_topic_1_4"
],
"duplicateKeys": [],
"emptySections": []
}

View 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.

View 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.

View 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

View 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

View 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.

View 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 persons 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 workflows final approval role from this slot
### Authorized By
- resolve from the latest approved action for the workflows 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

View 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

View 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

View 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.

View 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

View 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

View 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.

View File

@@ -0,0 +1,8 @@
Current:
branch stored in ms_options
Future:
organization_branches table
Reason:
branch will contain business metadata and workflow configuration

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,231 @@
{
"generatedAt": "2026-06-19T10:12:54.188Z",
"overallStatus": "PASS",
"quotationCode": "QT-H5-AUDIT",
"expectedFixtureCode": "QT-H5-AUDIT",
"template": {
"templateName": "ALLA Quotation Standard",
"version": "1.1",
"versionId": "bd6713d5-a731-482f-a5bb-0dea9d766301",
"fieldCount": 44,
"previousVersion": "1.0",
"deletedFields": [
"exclusion_data",
"exclusion_label"
],
"newFields": [],
"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": 5,
"itemCount": 3,
"partyCount": 3,
"dynamicTopicFieldCount": 5,
"dynamicTopicItemFieldCount": 5,
"mappingCount": 23
},
"mappingCoverage": {
"placeholderTokenCount": 12,
"unmappedTokens": [],
"orphanMappings": [],
"priceTableMapping": {
"sourcePath": "pdfme.quotation_price_data",
"dataType": "table"
},
"itemTableColumns": []
},
"runtimePayload": {
"templateInputKeys": 35,
"topicInputKeys": 10,
"priceTableRows": 1
},
"consistency": {
"status": "PASS",
"approvedSnapshotAvailable": true,
"approvedArtifactReference": "artifact:a1fd951d-f0b4-4e5b-be88-073596b3829d",
"inconsistentKeys": []
},
"integritySummary": {
"available": true,
"artifactDir": "C:\\Users\\mtpphtaps\\Documents\\gitea\\alla-allaos-fullstack\\artifacts\\pdf-audit"
}
}

View File

@@ -0,0 +1,31 @@
# PDF Audit Report
- Generated At: 2026-06-19T10:12:54.188Z
- Overall Status: PASS
- Quotation Code: QT-H5-AUDIT
- Template: ALLA Quotation Standard v1.1
- Previous Version: 1.0
## Metrics
- Topic Count: 5
- Item Count: 3
- Project Party Count: 3
- 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

View 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.

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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.

View 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

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View 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`

View File

@@ -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.

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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.

View File

@@ -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`

View File

@@ -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`

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View 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`

View 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`

View 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.

View File

@@ -0,0 +1,125 @@
# Task G - Quotation Document Preview Foundation
## Files Added
- `src/features/foundation/document-template/types.ts`
- `src/features/foundation/document-template/service.ts`
- `src/features/foundation/document-template/queries.ts`
- `src/features/foundation/document-template/mutations.ts`
- `src/features/foundation/document-template/server/service.ts`
- `src/features/foundation/document-template/components/template-settings.tsx`
- `src/features/crm/quotations/document/types.ts`
- `src/features/crm/quotations/document/service.ts`
- `src/features/crm/quotations/document/queries.ts`
- `src/features/crm/quotations/document/server/service.ts`
- `src/features/crm/quotations/components/quotation-document-preview.tsx`
- `src/app/api/crm/quotations/[id]/document-data/route.ts`
- `src/app/api/crm/quotations/[id]/document-preview/route.ts`
- `src/app/api/crm/document-templates/route.ts`
- `src/app/api/crm/document-templates/[id]/route.ts`
- `src/app/api/crm/document-templates/[id]/versions/route.ts`
- `drizzle/0006_worried_mordo.sql`
## Files Modified
- `src/db/schema.ts`
- `src/db/seeds/foundation.seed.ts`
- `src/lib/auth/rbac.ts`
- `src/config/nav-config.ts`
- `src/app/dashboard/crm/settings/templates/page.tsx`
- `src/app/dashboard/crm/quotations/[id]/page.tsx`
- `src/features/crm/quotations/components/quotation-detail.tsx`
- `src/features/crm/quotations/api/mutations.ts`
- `src/features/foundation/approval/mutations.ts`
- `docs/implementation/technical-debt.md`
## Schema Added
- `crm_document_templates`
- `crm_document_template_versions`
- `crm_document_template_mappings`
- `crm_document_template_table_columns`
## API Routes Added
- `GET /api/crm/quotations/[id]/document-data`
- `GET /api/crm/quotations/[id]/document-preview`
- `GET/POST /api/crm/document-templates`
- `GET/PATCH/DELETE /api/crm/document-templates/[id]`
- `GET/POST /api/crm/document-templates/[id]/versions`
## Preview UI Added
- Replaced quotation `Document Preview` placeholder with a production preview tab backed by real quotation data, template resolution, and mapped preview payloads.
- Added watermark/status behavior for non-approved quotations through normalized `watermarkStatus`.
- Preview now shows:
- quotation header
- customer block
- project info
- items table
- price summary
- scope / exclusion / payment topics
- approval block
- signature placeholders
## Template Settings UI Added
- Replaced `/dashboard/crm/settings/templates` placeholder with a production listing page.
- Current UI is intentionally lightweight and read-focused:
- template name and description
- default badge
- active/inactive badge
- file type
- latest version
- version count
- mapping count
## Template Seed Added
- Foundation seed now upserts a default quotation `pdfme` template per organization.
- Seed picks template JSON by organization name:
- `ALLA_template_pdfme_fainal3.json`
- `ONVALLA_template_pdfme_fainal3.json`
- If the organization name does not match either branch, the seed falls back to a minimal schema payload.
- Seed also creates baseline placeholder mappings and a table mapping for quotation line items.
## Mapping Resolver Added
- `resolveTemplateForDocument()`
- `resolveTemplateMappings()`
- `mapDocumentDataToTemplateInput()`
Behavior:
- resolves by `documentType + productType + fileType`
- falls back to `default` product type when no specific product template exists
- maps dot-path source fields into scalar, multiline, or table-ready template input
## Approved Snapshot Prep Added
- `prepareApprovedQuotationSnapshot(quotationId, organizationId)`
Output contains:
- `quotationId`
- `approvedAt`
- `documentData`
- `templateVersionId`
- `templateInput`
## Remaining Risks
- Mapping CRUD UI is not implemented yet, so template mappings remain seed-driven and read-only.
- `pdfme` preview is not canvas-accurate rendering; current preview is an application-native document view using the normalized document snapshot.
- Template resolution currently uses `productType: default` for quotation preview. Product-specific template selection can be added later once the business rule is finalized.
- No binary PDF generation, storage, or approved PDF URL persistence yet.
## Task H Readiness
Task G leaves the repo ready for:
- actual `pdfme` generation service integration
- approved PDF snapshot persistence
- template version publishing workflow
- mapping editor or admin maintenance UI
- product-specific template resolution rules

View File

@@ -0,0 +1,135 @@
# Task H - PDF Generation and Approved Document Persistence
## Files Added
- `src/features/foundation/pdf-generator/server/service.ts`
- `src/app/api/crm/quotations/[id]/pdf-preview/route.ts`
- `src/app/api/crm/quotations/[id]/pdf-download/route.ts`
- `src/app/api/crm/quotations/[id]/approved-pdf/route.ts`
- `drizzle/0007_luxuriant_malice.sql`
## Files Modified
- `package.json`
- `package-lock.json`
- `src/components/icons.tsx`
- `src/db/schema.ts`
- `src/lib/auth/rbac.ts`
- `src/features/crm/quotations/api/types.ts`
- `src/features/crm/quotations/server/service.ts`
- `src/features/crm/quotations/document/types.ts`
- `src/features/crm/quotations/document/server/service.ts`
- `src/features/foundation/approval/server/service.ts`
- `src/app/dashboard/crm/quotations/[id]/page.tsx`
- `src/features/crm/quotations/components/quotation-detail.tsx`
- `src/features/crm/enquiries/components/enquiry-form-sheet.tsx`
- `src/features/crm/enquiries/schemas/enquiry.schema.ts`
- `src/features/crm/quotations/schemas/quotation.schema.ts`
- `docs/implementation/technical-debt.md`
## PDF Generator Added
- Added production server-side `pdfme` generation in `src/features/foundation/pdf-generator/server/service.ts`.
- Installed:
- `@pdfme/common`
- `@pdfme/generator`
- `@pdfme/schemas`
- Implemented:
- `generatePdfFromTemplate()`
- `generateQuotationPdf()`
- `generateQuotationPreviewPdf()`
- `generateApprovedQuotationPdf()`
- `getStoredApprovedQuotationPdf()`
## API Routes Added
- `GET /api/crm/quotations/[id]/pdf-preview`
- `GET /api/crm/quotations/[id]/pdf-download`
- `GET /api/crm/quotations/[id]/approved-pdf`
- `POST /api/crm/quotations/[id]/approved-pdf`
Behavior:
- preview returns inline PDF from current quotation state
- download returns attachment PDF from current quotation state
- approved-pdf POST generates, persists, audits, and returns the approved PDF
- approved-pdf GET streams the already stored approved artifact
## UI Actions Added
Quotation detail now supports:
- `Preview PDF`
- `Download PDF`
- `Generate Approved PDF`
- `View Approved PDF`
The detail page also now displays:
- `Approved At`
- `Approved Template Version`
## Approved Snapshot Persistence
Added quotation persistence fields:
- `approvedAt`
- `approvedPdfUrl`
- `approvedSnapshot`
- `approvedTemplateVersionId`
Approved snapshot now stores:
- quotation id
- approved timestamp
- normalized document data
- template version id
- mapped template input
- generated timestamp
- generated by user id
## Attachment Metadata Integration
After generating an approved PDF:
- a local file is written under `public/generated/quotations/<organizationId>/`
- quotation attachment metadata is created or updated with:
- `fileName`
- `originalFileName`
- `filePath`
- `fileSize`
- `fileType = application/pdf`
- `description = Approved quotation PDF`
- `uploadedBy`
- `uploadedAt`
## Permission Added
- `crm.quotation.pdf.preview`
- `crm.quotation.pdf.download`
- `crm.quotation.pdf.generate_approved`
## Audit Integration
Required audit was added for approved PDF generation with:
- `entityType = crm_quotation_pdf_generate_approved`
- `action = generate_approved`
Preview and download remain unaudited by design.
## Remaining Risks
- Current `pdfme` generation uses a font fallback normalization step because the provided templates reference `cordia/cordiaBold`, while direct TTC-based rendering did not behave reliably with table layout generation.
- Approved PDF storage uses local filesystem placeholder output under `public/generated/...`, not external object storage.
- The environment used during implementation had no quotation rows available in the database, so full quotation-specific end-to-end smoke testing through a live record could not be completed here.
## Task I Readiness
Task H leaves the repo ready for:
- auto-generate approved PDF after final approval
- object-storage abstraction
- signed download / retention policy
- immutable approved artifact lifecycle rules
- richer template version publish controls

View File

@@ -0,0 +1,122 @@
# Task H.1: PDF Generation Stabilization
## 1. Files Added
- `scripts/seed-task-h1-fixture.js`
- `scripts/verify-task-h1.js`
- `docs/adr/0009-approved-document-storage-lifecycle.md`
## 2. Files Modified
- `package.json`
- `docs/implementation/technical-debt.md`
Task H.1 builds on the Task H PDF pipeline and focuses on validation, fixture support, documentation, and storage-lifecycle clarification.
## 3. Smoke Test Record Used
Added a dev-only fixture script for a real quotation scenario:
- approved fixture code: `QT-TASK-H1-APPROVED`
- draft fixture code: `QT-TASK-H1-DRAFT`
- customer fixture code: `CUS-TASK-H1`
Fixture script provisions:
- organization-scoped customer and contact
- approved quotation with item, customer role, scope/exclusion/payment topics, and completed approval history
- draft quotation for negative approved-PDF validation
- test users for permission checks
## 4. PDF APIs Verified
Task H.1 verification script covers:
- `GET /api/crm/quotations/[id]/pdf-preview`
- `GET /api/crm/quotations/[id]/pdf-download`
- `POST /api/crm/quotations/[id]/approved-pdf`
- `GET /api/crm/quotations/[id]/approved-pdf`
Expected checks included:
- preview responds as inline PDF
- download responds as attachment PDF
- approved-PDF POST succeeds for approved quotation
- approved-PDF GET returns persisted artifact
- non-approved quotation cannot generate approved PDF
## 5. Persistence Verified
Task H.1 verification targets:
- `approvedPdfUrl`
- `approvedSnapshot`
- `approvedTemplateVersionId`
- quotation attachment metadata row
- audit log row
- generated file under `public/generated/quotations/<organizationId>/`
## 6. Permission Verified
Fixture and verification support explicit checks for:
- `super_admin`
- admin workspace member
- regular user with PDF permissions
- regular user without PDF permissions
Permissions covered:
- `crm.quotation.pdf.preview`
- `crm.quotation.pdf.download`
- `crm.quotation.pdf.generate_approved`
## 7. Error Handling / Hardening
Task H.1 centered hardening around safe operational verification rather than a large refactor.
The main stabilization additions were:
- reusable dev fixture creation
- repeatable HTTP verification script
- clearer storage lifecycle documentation
- explicit debt tracking for fonts, local storage, and approved-artifact policy
## 8. ADR / Technical Debt Updated
Added ADR:
- `docs/adr/0009-approved-document-storage-lifecycle.md`
Updated debt notes in:
- `docs/implementation/technical-debt.md`
Tracked concerns include:
- font compatibility fallback
- local filesystem storage limitation
- approved artifact immutability policy
- post-approval automation gap
- missing CI-backed PDF E2E coverage
## 9. Remaining Risks
- approved PDF storage still uses local public filesystem semantics and is not production-grade object storage
- approved artifact lifecycle is still not immutable by policy
- PDF verification is script-based and not yet part of CI
- task fixture scripts currently depend on a prepared local environment and seeded master-option / approval data
## 10. Task I Readiness
Task H.1 leaves the PDF flow better prepared for Task I by providing:
- a repeatable quotation fixture path
- a repeatable verification script
- explicit storage-lifecycle decision documentation
- visible technical-debt boundaries for future production hardening
## 11. Verification Commands
- `npm run seed:task-h1-fixture`
- `npm run verify:task-h1`

View File

@@ -0,0 +1,87 @@
# Task H.2: PDFME Dynamic Topic Engine + Mapping Hotfix
## Files Added
- `src/features/crm/quotations/document/server/pdfme-transforms.ts`
- `src/features/crm/quotations/document/server/pdf-topic.type.ts`
- `src/features/crm/quotations/document/server/pdf-topic-engine.ts`
- `src/features/crm/quotations/document/server/topic-mapping.ts`
- `docs/implementation/task-h2-pdfme-mapping-transform-hotfix.md`
## Files Modified
- `src/features/crm/quotations/document/server/service.ts`
- `src/features/crm/quotations/document/types.ts`
- `src/features/crm/quotations/components/quotation-document-preview.tsx`
- `src/features/foundation/document-template/server/service.ts`
- `src/db/seeds/foundation.seed.ts`
- `scripts/verify-task-h1.js`
- `scripts/audit-pdfme-runtime.ts`
- `docs/implementation/technical-debt.md`
## Transform Utilities Added
- `formatPdfDate()`
- `formatPdfCurrency()`
- `formatTopicItems()`
- `normalizePdfmeTable()`
## Dynamic Topic Engine Added
- page-2 `topic` and `data_topic` schemas are now treated as dynamic templates instead of static mappings
- topic blocks are cloned into runtime keys like `topic_1_0` and `item_topic_1_0`
- topic rows are normalized into `string[][]`
- the closing/signature block is kept together and pushed to the next page when remaining space is too small
- long topic lists can expand page 2 into multiple generated pages
## Product Topic Mapping Added
- product-type-aware topic mapping with `crane`, `dockdoor`, `solarcell`, and `default` fallbacks
- alias matching allows current simplified option codes and legacy-style topic codes to resolve into PDFME-ready sections
## Template Mappings Fixed
- `customer_att -> quotation.attention`
- `quotation_date -> pdfme.quotation_date`
- `quotation_price -> pdfme.quotation_price`
- `exclusion_data -> pdfme.exclusion_data`
- legacy `item_topic` mapping is now explicitly retired from foundation seed data because dynamic topics are schema-driven
## Seed Mapping Strategy After Dynamic Topic Engine
- default PDFME seed mappings now keep only static runtime fields plus safe signature placeholders
- `quotation_date` and `quotation_price` are seeded against the preformatted `pdfme.*` fields with no extra format mask
- `topic`, `data_topic`, `item_topic`, and wildcard runtime keys such as `topic_*` / `item_topic_*` are retired from DB mappings
- signature placeholders `app1/app2/app3` and position fields are seeded with safe `defaultValue: "-"` until Task H.3 defines the real approval-signature strategy
## PDF Generator Integration
- quotation preview data now returns a render-ready PDFME schema with injected topic pages
- `templateInput` now merges dynamic topic inputs with normal mapped fields
- approved snapshot generation now captures the same merged topic input set used by PDF generation
- the existing PDF generator flow now consumes the render-ready schema returned by the preview service
## Verification Result
- `npx tsc --noEmit` passed
- `scripts/verify-task-h1.js` now asserts:
- formatted `quotation_price`
- formatted `quotation_date`
- non-empty `exclusion_data`
- generated dynamic topic keys in preview payload
- `documentData.pdfme.topic_inputs`
- no unresolved `{exclusion_data}`, `{quotation_price}`, and `{item_topic}` in approved PDF text
- inline `tsx` smoke testing for the new engine was inconclusive because the local script loader exposed the module as a default-only object even though project typecheck succeeded
- full `verify:task-h1` end-to-end execution was not run in this turn because it depends on a live app server and fixture database state
## Remaining PDFME Gaps
- approval signature placeholders are still not mapped to real approver names/positions
- pixel-perfect parity with the legacy layout has not been regression-tested automatically
- `scripts/audit-pdfme-runtime.ts` still needs a stable standalone runtime strategy if we want to use it outside the Next.js execution context
## Task K Readiness
- dynamic server-side topic rendering now exists, so follow-on PDF tasks can build on runtime schema generation instead of adding more fixed placeholders
- seed data and template mapping are aligned with the current ALLA / ONVALLA templates
- the next safe step is end-to-end fixture verification against a running app plus any remaining signature-field strategy

View File

@@ -0,0 +1,78 @@
# Task H.3: Approval Signature Strategy
## Files Added
- `src/features/crm/quotations/document/signature.types.ts`
- `src/features/crm/quotations/document/signature-position.ts`
- `src/features/crm/quotations/document/server/signature-resolver.ts`
- `scripts/verify-task-h3.js`
- `docs/adr/0012-approval-signature-strategy.md`
- `docs/implementation/task-h3-approval-signature-strategy.md`
## Files Modified
- `src/features/crm/quotations/document/types.ts`
- `src/features/crm/quotations/document/server/service.ts`
- `src/features/crm/quotations/components/quotation-document-preview.tsx`
- `src/features/foundation/pdf-generator/server/service.ts`
- `src/db/seeds/foundation.seed.ts`
- `docs/implementation/technical-debt.md`
- `package.json`
## Signature Resolution Strategy
- `Prepared By` resolves from `quotation.salesmanId`, then falls back to `quotation.createdBy`
- `Approved By` resolves from the latest approved workflow action for `sales_manager` or `department_manager`, excluding the final authorization step
- `Authorized By` resolves from the latest approved workflow action for the final workflow step, which is `top_manager` in the standard flow
- each signature block now carries:
- `name`
- `position`
- `actedAt`
## Position Resolution Strategy
- primary source is `memberships.businessRole`
- display label first checks active `crm_job_title` master options
- fallback labels are:
- `sales` -> `Sales Engineer`
- `sales_support` -> `Sales Support`
- `sales_manager` -> `Sales Manager`
- `department_manager` -> `Department Manager`
- `top_manager` -> `General Manager`
## PDF Mapping Changes
- `app1` -> `signatures.preparedBy.name`
- `app1_position` -> `signatures.preparedBy.position`
- `app2` -> `signatures.approvedBy.name`
- `app2_position` -> `signatures.approvedBy.position`
- `app3` -> `signatures.authorizedBy.name`
- `app3_position` -> `signatures.authorizedBy.position`
## Snapshot Changes
- `prepareApprovedQuotationSnapshot()` now stores the resolved signature block through `documentData`
- persisted `templateInput` now preserves the text values used for `app1/app2/app3`
- approved PDF generation prefers snapshot input when a snapshot already exists
## UI Changes
- quotation document preview now shows an `Approval Signatures` section
- each block displays:
- name
- position
- timestamp when available
## Verification Result
- `scripts/verify-task-h3.js` validates:
- DB mappings for `app1/app2/app3`
- `crm_job_title` master options
- approved snapshot signature payload
- approved snapshot template input alignment
## Remaining Risks
- organizations with custom approval roles outside the current sales-manager / department-manager / final-step model may need an extended resolver rule later
- `Prepared By` currently uses quotation `createdAt` as the available timestamp because the system does not yet store a separate “prepared at” event
- end-to-end PDF binary text extraction is still not automated; verification currently proves snapshot and template-input integrity

View File

@@ -0,0 +1,42 @@
# Task H.5: PDF Mapping Audit & Visual Parity
## Files Added
- `scripts/pdf-audit-utils.ts`
- `scripts/audit-pdf-template-inventory.ts`
- `scripts/audit-pdf-mapping-coverage.ts`
- `scripts/audit-pdf-runtime-payload.ts`
- `scripts/generate-pdf-audit-report.ts`
- `docs/implementation/pdf-parity-checklist.md`
- `docs/business/pdf-mapping-registry.md`
- `docs/adr/0013-pdf-visual-parity-strategy.md`
- `docs/implementation/task-h5-pdf-mapping-audit-visual-parity.md`
## Files Modified
- `package.json`
- `scripts/seed-task-h1-fixture.js`
## Audit Coverage
- template inventory audit scans active template versions, schema field names, table fields, dynamic topic fields, duplicates, missing mappings, and orphan mappings
- mapping coverage audit compares placeholder tokens with DB mappings and table-column metadata
- runtime payload audit inspects resolved `documentData`, `templateInput`, and `pdfme.topic_inputs`
- report generator writes JSON and Markdown summary artifacts for regression review
## Fixture Strategy
- `seed-task-h1-fixture.js` now also provisions `QT-H5-AUDIT`
- the audit fixture is intended to carry richer topic/item/party coverage than the original H.1 smoke dataset
## Remaining Risks
- approved snapshot parity remains a warning when the target quotation has never generated an approved PDF artifact
- the audit suite validates business payload parity, not pixel-perfect PDF screenshots
- manual review is still needed when template designers move static labels or brand blocks
## H.5.3 Follow-up
- template JSON revisions now flow through a dedicated version-remap step instead of patching `1.0` in place
- static labels such as `tel_label`, `email_label`, `att_label`, `project_label`, and `site_label` are now runtime-mapped rather than tolerated as blank designer placeholders
- deleted fields from the previous version, notably `exclusion_data` and `exclusion_label`, are explicitly classified and removed from the active mapping set

View File

@@ -0,0 +1,51 @@
# Task H.5.1: PDF Integrity Audit & Payload Parity
## Files Added
- `scripts/audit-template-version-integrity.ts`
- `scripts/audit-placeholder-integrity.ts`
- `scripts/audit-payload-parity.ts`
- `scripts/audit-approved-snapshot-parity.ts`
- `scripts/generate-pdf-integrity-report.ts`
- `docs/business/pdf-placeholder-registry.md`
- `docs/implementation/task-h51-pdf-integrity-audit.md`
## Files Modified
- `package.json`
- `scripts/generate-pdf-audit-report.ts`
- `docs/implementation/pdf-parity-checklist.md`
- `docs/business/pdf-mapping-registry.md`
- `docs/implementation/technical-debt.md`
- `scripts/pdf-audit-utils.ts`
## Integrity Coverage
- template-version integrity checks active template uniqueness, active version uniqueness, orphan mappings, and approved template-version references
- placeholder integrity checks duplicate names, typo-like names, unmapped placeholders, dead mappings, and reserved dynamic topic names
- payload parity computes SHA256 hashes for preview, generation, and computed snapshot payloads
- approved snapshot parity compares stored snapshot payloads against computed snapshot payloads when they exist
- integrity summary writes machine-readable artifacts into `artifacts/pdf-audit/`
## Verification Result
- `npx tsc --noEmit` passed
- `npm run audit:pdf` runs the full H.5 + H.5.1 stack
- current integrity result is expected to be `WARNING` when the audit fixture has not yet generated a persisted approved snapshot/artifact
## Remaining Risks
- stored approved snapshot parity still depends on generating a real approved PDF artifact for `QT-H5-AUDIT`
- template field `line` is still treated as a tolerated designer-owned oddity rather than a normalized mapping field
- the audit stack validates payload integrity and runtime parity, not screenshot-level pixel parity
## Follow-on Freeze Note
- `quotation_price_data` is now treated as the canonical PDFME table field for quotation price rendering
- `quotation_price` and `currency` remain supported only as scalar compatibility fields
## H.5.3 Versioning Note
- integrity checks now accept retained inactive template versions as valid historical references for approved snapshots
- the active-version audit also verifies that the active DB schema matches the latest JSON source file for each organization template
- runtime payload integrity now includes explicit static-label checks so blank `tel_label`/`email_label`/`att_label`/`project_label`/`site_label` values fail the suite

View File

@@ -0,0 +1,33 @@
# Task H.5.3: PDF Template Version Remap + Static Labels
## Outcome
- created active `1.1` quotation PDF template versions for both `ALLA` and `ONVALLA`
- retained historical `1.0` versions as inactive records
- rebuilt active mappings from the current template inventory instead of copying the old version wholesale
- restored runtime values for `tel_label`, `email_label`, `att_label`, `project_label`, and `site_label`
## Field Inventory Result
- deleted from previous version: `exclusion_data`, `exclusion_label`
- new active templates keep dynamic topic bases `topic` and `data_topic`
- designer-owned static visuals such as `line`, `quotation_date_labe`, and `quotation_code_lable` remain classified but unmapped
- compatibility tokens `quotation_price` and `currency` remain mapped because the active price table still embeds those placeholders
## Seed / Remap Flow
- `npm run seed:pdf-template-version` reads the latest JSON from `src/pdfme_template/*.json`
- if the schema already exists in DB, it re-activates and reconciles mappings idempotently
- if the schema is new, it creates the next minor version, activates it, and deactivates the previous active version
- mappings are rebuilt only for fields or tokens still present in that target version
## Verification
- `npx tsc --noEmit` passed
- `npm run seed:pdf-template-version` passed
- `npm run audit:pdf` passed with overall status `PASS`
## Remaining Risks
- active templates still rely on compatibility tokens `quotation_price` and `currency` inside the table content; those should be removed only in a future controlled template revision
- the audit fixture now points to active `1.1`; production-approved historical quotations still need operator discipline so their stored template version is never rewritten manually

View File

@@ -0,0 +1,155 @@
# Task I: Storage Abstraction and Approved Artifact Lifecycle
## 1. Files Added
- `src/features/foundation/storage/types.ts`
- `src/features/foundation/storage/service.ts`
- `src/features/foundation/storage/server/local-provider.ts`
- `src/features/foundation/storage/server/s3-provider.ts`
- `src/features/foundation/storage/server/provider.ts`
- `src/features/foundation/storage/server/checksum.ts`
- `src/features/foundation/document-artifact/server/service.ts`
- `src/app/api/crm/document-artifacts/[id]/download/route.ts`
- `drizzle/0009_lazy_shard.sql`
- `drizzle/meta/0009_snapshot.json`
## 2. Files Modified
- `package.json`
- `package-lock.json`
- `src/db/schema.ts`
- `src/lib/auth/rbac.ts`
- `src/features/foundation/pdf-generator/server/service.ts`
- `src/features/foundation/approval/server/service.ts`
- `src/features/crm/quotations/api/types.ts`
- `src/features/crm/quotations/server/service.ts`
- `src/features/crm/quotations/components/quotation-detail.tsx`
- `src/app/api/crm/quotations/[id]/approved-pdf/route.ts`
- `scripts/seed-task-h1-fixture.js`
- `docs/implementation/technical-debt.md`
- `docs/adr/0009-approved-document-storage-lifecycle.md`
## 3. Storage Provider Added
Added a storage foundation under `src/features/foundation/storage/**`.
Providers:
- local provider for development
- S3 / MinIO-compatible provider for production-oriented storage
Configuration supported:
- `STORAGE_PROVIDER`
- `LOCAL_STORAGE_ROOT`
- `S3_ENDPOINT`
- `S3_REGION`
- `S3_BUCKET`
- `S3_ACCESS_KEY_ID`
- `S3_SECRET_ACCESS_KEY`
- `S3_FORCE_PATH_STYLE`
## 4. Artifact Schema Added
Added `crm_document_artifacts` with:
- organization/entity identity
- document and artifact types
- template version reference
- storage provider and storage key
- file metadata
- checksum
- lifecycle status
- generated / locked / voided metadata
- JSON metadata payload
Added `approved_artifact_id` to `crm_quotations`.
## 5. Approved PDF Refactor
Approved quotation PDFs now:
- write through `StorageProvider.putObject()`
- persist checksum via SHA-256
- create a `crm_document_artifacts` row
- lock the artifact after generation
- update quotation `approvedArtifactId`
- keep `approvedPdfUrl` as compatibility reference in `artifact:<artifactId>` form
Rules enforced:
- only approved quotations can generate approved PDF
- locked approved artifacts cannot be overwritten
- existing active approved artifact blocks silent regeneration
## 6. Secure Download Added
Added secure artifact route:
- `GET /api/crm/document-artifacts/[id]/download`
Updated quotation approved-PDF flow so secure reads resolve artifact metadata from the database and fetch bytes through the storage provider instead of treating public file paths as source of truth.
## 7. UI Updated
Quotation detail now shows approved artifact metadata when available:
- file name
- generated by
- generated at
- checksum short form
- locked status badge
It also shows a legacy warning when a quotation still references old `/generated/...` storage without an artifact record.
## 8. Permissions Added
- `crm.document_artifact.read`
- `crm.document_artifact.download`
- `crm.document_artifact.void`
Defaults were added alongside the existing quotation PDF permissions in RBAC role derivation.
## 9. Audit Integration
Artifact service now audits:
- `create`
- `lock`
- `void`
- `download`
Entity type:
- `crm_document_artifact`
## 10. Compatibility Notes
Compatibility behavior now supports:
- `approvedArtifactId` when present
- `approvedPdfUrl = artifact:<artifactId>` during migration
- legacy `/generated/...` approved PDF paths with warning fallback
Task I intentionally does not bulk-migrate legacy files yet.
## 11. Remaining Risks
- artifact void/regeneration still has no admin UI workflow
- legacy approved PDFs still need one-time migration into artifact storage
- signed-URL delivery is not enabled yet; downloads are still server-streamed
- storage-provider health and bucket policy checks are still operational concerns outside the app
## 12. Task J Readiness
Task I leaves the app ready for:
- richer artifact lifecycle tooling
- legacy-file migration command
- object-storage rollout beyond local development
- future secure delivery strategies such as signed URLs
## 13. Verification
- `npx tsc --noEmit`
- `npm run gen`

View File

@@ -0,0 +1,74 @@
# Task J: CRM Dashboard KPI and Sales Analytics
## Files added
- `src/features/crm/dashboard/api/types.ts`
- `src/features/crm/dashboard/api/service.ts`
- `src/features/crm/dashboard/api/queries.ts`
- `src/features/crm/dashboard/server/service.ts`
- `src/features/crm/dashboard/components/crm-dashboard.tsx`
- `src/features/crm/dashboard/components/dashboard-filters.tsx`
- `src/features/crm/dashboard/components/dashboard-summary-cards.tsx`
- `src/features/crm/dashboard/components/dashboard-funnel.tsx`
- `src/features/crm/dashboard/components/dashboard-revenue-cards.tsx`
- `src/features/crm/dashboard/components/dashboard-sales-ranking.tsx`
- `src/features/crm/dashboard/components/dashboard-followups.tsx`
- `src/features/crm/dashboard/components/dashboard-approval-metrics.tsx`
- `src/features/crm/dashboard/components/dashboard-hot-projects.tsx`
- `src/app/api/crm/dashboard/route.ts`
- `src/app/api/crm/dashboard/export/route.ts`
## Files modified
- `src/app/dashboard/crm/page.tsx`
- `src/config/nav-config.ts`
- `src/lib/auth/rbac.ts`
- `src/lib/searchparams.ts`
- `docs/implementation/technical-debt.md`
## KPI widgets added
- Replaced the CRM placeholder route with a production dashboard at `/dashboard/crm`.
- Added summary cards for lead, opportunity, quotation, and revenue metrics.
- Added a funnel widget for lead -> opportunity -> quotation -> pending approval -> approved -> closed won.
- Added a hot-project widget based on real enquiry data.
## Revenue analytics added
- Added top end-customer, billing-customer, contractor, and consultant revenue tables.
- Reused the Task D.2.1 reporting helpers so revenue attribution follows the frozen governance rules.
- Added export support for revenue analytics through the dashboard export route.
## Sales ranking added
- Added a production sales ranking table using enquiry assignment and quotation salesman ownership rules.
- Added CSV export for sales ranking.
## Approval and follow-up analytics added
- Added approval KPI cards and a `My Pending Approvals` table.
- Added follow-up KPI cards and an `Upcoming Follow Ups` table from enquiry and quotation follow-up sources.
## Export and permissions added
- Added `crm.dashboard.read`.
- Added `crm.dashboard.export`.
- Added audit logging for `crm_dashboard_export` with `export_csv` and `export_excel`.
- Added sidebar navigation for the CRM dashboard.
## Remaining risks
- `closed_won` is not yet a first-class enquiry lifecycle field, so the dashboard currently uses quotation status `accepted` as the practical won-stage proxy.
- follow-up completion is inferred from `outcome` presence because the follow-up model still has no explicit completion flag.
- the global `Project Party Role` filter currently influences revenue analytics and exports, but the rest of the dashboard still depends mainly on enquiry and quotation core filters.
- sales users currently read organization-wide dashboard data; own-scope filtering is still future work.
## Task K readiness
- the CRM dashboard now uses real production data
- KPI widgets are live
- revenue analytics is reusable
- sales ranking is exportable
- approval and follow-up analytics exist
- export and audit hooks are in place
- the app is ready for Task K report-center expansion

View File

@@ -0,0 +1,139 @@
# Task J.0: KPI Definition Freeze
## Scope delivered
- Froze the CRM sales pipeline definitions that Dashboard KPI must use.
- Froze customer party terminology so enquiry, quotation, and dashboard reporting use the same business language.
- Froze KPI and revenue formulas before Task J dashboard work begins.
- Declared that KPI definition changes after this point require a new ADR or equivalent decision record.
## Governance rule
- After Task J.0, KPI definitions, pipeline meanings, and dashboard counting rules must not change silently.
- Any future change to these definitions requires a new ADR so reporting behavior stays auditable.
## Frozen sales pipeline
### Lead
- Definition: enquiry is not assigned to sales yet.
- Rule: `assignedToUserId = null`
- Rule: `pipelineStage = lead`
- KPI impact: `Lead Count`, `Lead Aging`, `Lead Source`, `New Leads`, `Unassigned Leads`
### Opportunity
- Definition: enquiry has already been assigned to sales.
- Rule: `assignedToUserId != null`
- Rule: `pipelineStage = opportunity`
- Auto transition: assigning sales moves the record from `Lead` to `Opportunity`
- KPI impact: `Opportunity Count`, `Opportunity Value`, `Hot Opportunities`, `Opportunity Aging`, `Open Opportunities`
### Closed Won
- Definition: customer confirms the award, or the project is officially awarded.
- Rule: `pipelineStage = closed_won`
### Closed Lost
- Definition: competitor wins, budget is not approved, customer cancels, or project does not proceed.
- Rule: `pipelineStage = closed_lost`
## Frozen opportunity lifecycle
```txt
Lead
-> Assign Sales
Opportunity
-> Create Quotation
Opportunity
-> Approved Quotation
Opportunity
-> Award
Closed Won
```
## Frozen customer party definitions
- `Billing Customer`: the company used for quotation issue and billing.
- `End Customer`: the real project owner.
- `Contractor`: the construction contractor or main contractor.
- `Consultant`: the project consultant.
- `Customer`: a general commercial counterparty or related party when a more specific role is not applicable.
These terms are frozen as the shared language for CRM forms, detail views, and future dashboard/reporting logic.
## Frozen KPI definitions
### Lead metrics
- `Lead Count` = enquiries where `pipelineStage = lead`
- `New Leads` = lead-stage enquiries created within the selected reporting period
- `Unassigned Leads` = lead-stage enquiries where `assignedToUserId = null`
- `Lead Aging` = age of lead-stage enquiries based on creation date until current/report date
### Opportunity metrics
- `Opportunity Count` = enquiries where `pipelineStage = opportunity`
- `Open Opportunities` = opportunities still active and not moved to `closed_won` or `closed_lost`
- `Hot Opportunities` = opportunity-stage enquiries flagged by the future dashboard/business rule layer
- `Opportunity Aging` = age of opportunity-stage enquiries based on entry into active opportunity lifecycle until current/report date
### Conversion metrics
- `Lead -> Opportunity` = `Opportunity Created / Total Leads`
- `Opportunity -> Quotation` = `Opportunity With Quotation / Total Opportunities`
- `Quotation -> Won` = `Closed Won / Total Opportunities With Quotation`
## Frozen revenue rules
### Opportunity value
- Source: `crm_enquiries.estimatedValue`
- Condition: only count rows where `pipelineStage = opportunity`
### Quotation value
- Source: `crm_quotations.totalAmount`
- Condition: count quotations where `status != cancelled`
### Won value
- Source: `crm_quotations.totalAmount`
- Condition: count quotation-linked business only when parent enquiry `pipelineStage = closed_won`
### Lost value
- Source: `crm_quotations.totalAmount`
- Condition: count quotation-linked business only when parent enquiry `pipelineStage = closed_lost`
## Frozen ownership rules for reporting
- Dashboard ownership for `Lead` and `Opportunity` must use `assignedToUserId`.
- Dashboard ownership for `Quotation` must use `salesmanId`.
This prevents mixing enquiry ownership with quotation ownership in the same KPI bucket.
## Explicit non-scope items
Task J.0 intentionally does not add lifecycle timestamp fields or new master-data defaults.
Deferred fields:
- `convertedToOpportunityAt`
- `closedWonAt`
- `closedLostAt`
- `lostReason`
- `defaultPartyRole`
These remain technical debt and should be added only when downstream reporting or workflow requirements justify schema changes.
## Dashboard readiness
Task J can now build CRM dashboard KPIs on top of these frozen definitions:
- KPI definitions frozen
- sales pipeline frozen
- customer party definitions frozen
- revenue rules frozen
- dashboard semantics ready for implementation

View File

@@ -0,0 +1,85 @@
# Task J.1: CRM Admin Configuration Center
## Files Added
- `src/app/api/crm/settings/**`
- `src/app/dashboard/crm/settings/approval-workflows/page.tsx`
- `src/features/foundation/approval/components/approval-workflow-settings.tsx`
- `src/features/foundation/document-sequence/client-service.ts`
- `src/features/foundation/document-sequence/components/document-sequence-settings.tsx`
- `src/features/foundation/document-sequence/mutations.ts`
- `src/features/foundation/document-sequence/queries.ts`
- `src/features/foundation/document-template/mutations.ts`
- `docs/implementation/task-j1-crm-admin-configuration-center.md`
## Files Modified
- `src/config/nav-config.ts`
- `src/lib/auth/rbac.ts`
- `src/app/dashboard/crm/settings/document-sequences/page.tsx`
- `src/features/foundation/approval/{mutations,queries,server/service,service,types}.ts`
- `src/features/foundation/document-sequence/{service,types}.ts`
- `src/features/foundation/document-template/{components/template-settings,mutations,server/service,service,types}.ts`
## What Was Added
- CRM Settings menu now includes `Approval Workflows` and uses CRM-specific permission checks for `Document Sequences`.
- `/dashboard/crm/settings/templates` now supports:
- create/edit template metadata
- create/edit template versions with JSON schema textarea
- create/edit/delete template mappings
- table-column mapping input for `table` data type
- `/dashboard/crm/settings/approval-workflows` now supports:
- create/edit/delete workflows
- manage sequential workflow steps
- role-based step definitions using CRM business roles
- `/dashboard/crm/settings/document-sequences` now supports:
- create/edit/delete sequences
- reset sequence counters
- preview next code from stored sequence state
## API Routes Added
- `/api/crm/settings/document-templates/**`
- `/api/crm/settings/approval-workflows/**`
- `/api/crm/settings/document-sequences/**`
## Permissions Added
- `crm.approval.workflow.read`
- `crm.approval.workflow.create`
- `crm.approval.workflow.update`
- `crm.approval.workflow.delete`
- `crm.document_sequence.read`
- `crm.document_sequence.create`
- `crm.document_sequence.update`
- `crm.document_sequence.reset`
## Audit Coverage
- `crm_document_template_version`
- `crm_document_template_mapping`
- `crm_approval_workflow`
- `crm_approval_step`
- `crm_document_sequence`
## Verification
- `npx tsc --noEmit` passed on June 17, 2026.
- `npm run lint` still fails because of pre-existing repo lint errors outside Task J.1 scope, including:
- `src/components/ui/input-group.tsx`
- `src/features/chat/components/message-composer.tsx`
- `src/components/forms/demo-form.tsx`
## Remaining Risks
- Template settings UI is functional but not yet a polished table-first admin experience.
- Document template version activation currently supports state updates, but the page focuses on version edit/create over dedicated activate/deactivate action buttons.
- Sequence reset UI currently resets to `0`; custom reset value can be added later if the product needs it.
- Approval step management currently replaces the full ordered step list in one save flow rather than exposing per-row move controls.
## Task K Readiness
- CRM settings foundations are now exposed through organization-scoped admin routes.
- Query invalidation is wired for template, workflow, and sequence mutations.
- The configuration center is ready for KPI/report consumers to reference the same settings data in later tasks.

View File

@@ -0,0 +1,39 @@
# Task K.1 Implementation Summary
## Completed
- added `crm_report_definitions` schema and migration
- seeded `crm_report_category` master options
- seeded system report definitions:
- `lead_pipeline`
- `enquiry_pipeline`
- `sales_performance`
- `lost_analysis`
- `revenue_by_customer`
- added permissions:
- `crm.report.read`
- `crm.report.export`
- created `src/features/crm/reports/` foundation with:
- API contracts
- shared query options
- `ResolvedReportContext`
- `resolveCrmAccess()`
- dataset layer
- builder layer
- shared filter metadata builder
- export helpers
- added report discovery APIs:
- `GET /api/crm/reports`
- `GET /api/crm/reports/catalog`
- `GET /api/crm/reports/filters`
- added audit logging for report foundation reads
- added CRM Reports landing page and sidebar navigation entry
## Verification
- `npm exec tsc --noEmit`
## Notes
- this task creates the shared report platform only; it does not implement business report execution pages yet
- export helpers are centralized and ready for future report-specific export routes

View File

@@ -0,0 +1,65 @@
# Task K.2 Pipeline Reports
## Delivered
- Added production CRM report APIs for:
- `GET /api/crm/reports/pipeline`
- `GET /api/crm/reports/lead-aging`
- `GET /api/crm/reports/enquiry-aging`
- `GET /api/crm/reports/export`
- Added production CRM report pages:
- `/dashboard/crm/reports/pipeline`
- `/dashboard/crm/reports/lead-aging`
- `/dashboard/crm/reports/enquiry-aging`
- Added shared dataset layer at `src/features/crm/reports/server/datasets/pipeline-report.dataset.ts`
- Extended report query contracts, React Query keys, and API client services for the new report suite
- Added report export support for CSV and Excel with scoped security enforcement
- Added audit logging for:
- `view_pipeline_report`
- `export_pipeline_report_csv`
- `export_pipeline_report_excel`
- Extended CRM report definitions seed with:
- `lead_pipeline`
- `lead_aging`
- `enquiry_pipeline`
- `enquiry_aging`
- `pipeline_funnel`
- `lead_conversion`
- `enquiry_conversion`
- Added report navigation entries under CRM side menu and report catalog deep links
## Security and Scope
- Report data is filtered by:
- organization
- branch scope
- product scope
- own-record scope for `ownershipScope = own`
- Revenue-like funnel values are hidden when the user does not have quotation pricing visibility
- Export uses the same filter contract and resolved access context as on-screen reports
## Report Coverage
- Pipeline suite page includes:
- Lead Pipeline
- Enquiry Pipeline
- Lead to Enquiry Conversion
- Enquiry to Quotation Conversion
- Pipeline Funnel
- Lead Aging page includes bucket summary and drill-down rows
- Enquiry Aging page includes bucket summary and drill-down rows with last follow-up visibility
## Verification
- `npx tsc --noEmit`
- passed
- `npx oxlint src/features/crm/reports src/app/api/crm/reports src/app/dashboard/crm/reports src/config/nav-config.ts`
- passed
- `npm run lint`
- still fails because of pre-existing repository-wide lint issues outside Task K.2 scope
## Notes
- Lead reporting is derived from the frozen lifecycle stored on `crm_enquiries`
- Lead conversion is treated as progression beyond `lead` stage or assignment into active sales handling
- Enquiry to quotation conversion is derived from `crm_quotations.enquiry_id`

View File

@@ -0,0 +1,60 @@
# Task L: CRM Permission & Role Management
## Summary
Task L starts the production CRM authorization foundation by replacing template-level behavior with organization-owned role profiles, effective permission resolution, and server-side scope enforcement.
## Implemented
- Added CRM role profile defaults for:
- `marketing`
- `sales`
- `sales_support`
- `sales_manager`
- `department_manager`
- `top_manager`
- `crm_admin`
- Added dedicated CRM permissions for:
- quotation pricing visibility
- role management
- master option management
- Added `crmRoleProfiles` persistence model with:
- permissions
- ownership scope
- branch scope mode
- product scope mode
- approval authority
- active/system flags
- Added membership scope fields:
- `branchScopeIds`
- `productTypeScopeIds`
- Added resolved CRM access helper to combine:
- system role
- membership role
- role profile permissions
- direct membership permissions
- Added CRM Settings > Roles page and role-management APIs for:
- role listing
- role editing
- permission matrix
- role cloning
- role activation/deactivation
- Added audit logging for CRM role create/update events.
- Moved CRM dashboard access evaluation to resolved branch/product/ownership scope.
- Started server-side enquiry enforcement for:
- branch scope
- product-type scope
- ownership visibility
- assign/reassign/follow-up access consistency
## Notes
- This task lays the authorization foundation first; dynamic user assignment UI for cloned CRM roles can build on top of these role profiles next.
- Quotation and approval flows should continue migrating onto the same resolved access model so ownership and scope rules stay consistent across CRM.
## Verification
- Run: `npx tsc --noEmit`
- Confirm CRM Settings sidebar shows `Roles & Permissions` only for users with `crm.role.read`
- Confirm sales users cannot access enquiries outside assigned branch/product scope
- Confirm dashboard export and dashboard summary honor resolved scope and permissions

View File

@@ -0,0 +1,46 @@
# Task L.1: CRM Multi-Role User Assignment
## Summary
Task L.1 extends the CRM authorization foundation so one user can hold multiple CRM role assignments inside the same organization.
## Implemented
- Added `crm_user_role_assignments` schema for CRM-specific authorization rows
- Added assignment permissions:
- `crm.role.assignment.read`
- `crm.role.assignment.create`
- `crm.role.assignment.update`
- `crm.role.assignment.delete`
- `crm.role.assignment.manage`
- Added lazy compatibility backfill from `memberships.businessRole` into CRM role assignments
- Updated CRM access resolution to:
- union permissions across all active assignments
- union branch/product scope across assignments
- choose the most permissive ownership scope
- choose the highest approval authority
- use primary role for display only
- Added `CRM Settings > User Role Assignments`
- Added assignment API routes:
- `GET /api/crm/settings/user-role-assignments`
- `POST /api/crm/settings/user-role-assignments`
- `PATCH /api/crm/settings/user-role-assignments/[id]`
- `DELETE /api/crm/settings/user-role-assignments/[id]`
- Added audit logging for `crm_user_role_assignment`
- Added ADR `0014-crm-multi-role-user-assignment`
## Compatibility
- `memberships.businessRole` remains in place
- when no active CRM role assignment exists, resolver still falls back safely
- when role assignments exist, CRM authorization no longer treats `memberships.businessRole` as the primary source
## Verification
- Run: `npx tsc --noEmit`
## Remaining Risks
- user-creation and user-edit flows still center around legacy `memberships.businessRole`
- some CRM modules beyond the current resolver consumers may still need deeper scenario verification with mixed-role fixtures
- a dedicated one-shot operational migration script can still be useful even though lazy backfill is active

View File

@@ -0,0 +1,45 @@
# Task L.2: User Management Integration
## Summary
Task L.2 moves user-management flows onto CRM role assignments so `memberships.businessRole` is no longer the primary CRM authorization source.
## Implemented
- Refactored user create/edit payloads to use:
- `memberships`
- `crmRoleAssignments`
- Updated user-management server utilities to:
- sync CRM role assignments during create/edit
- compute legacy `membership.businessRole` as a compatibility mirror from the primary assignment
- build user response models with CRM role summaries and resolved access summaries
- Updated user listing to show:
- primary CRM role
- CRM role count
- branch scope summary
- product scope summary
- Updated user form sheet to manage CRM role assignments directly:
- add role
- remove role
- set primary
- branch/product scope editing
- effective access preview inside the sheet
- Added user effective-access preview endpoint with audit logging
- Added CRM role-assignment reference endpoint for user-management forms
- Added migration utility:
- `scripts/migrate-membership-business-roles.ts`
## Compatibility
- `memberships.businessRole` still exists and is still populated
- when CRM assignments exist, resolved CRM authorization uses them as the primary source
- when no assignments exist, fallback remains functional and is surfaced in admin UI as legacy fallback
## Verification
- Run: `npx tsc --noEmit`
## Notes
- User management now acts as the primary operational screen for CRM role assignment, so admins do not need to leave the user workflow to adjust CRM authorization.
- The sheet-based effective access preview also serves as the administrator troubleshooting panel for mixed-role users.

View File

@@ -0,0 +1,37 @@
# Task L.3: Full CRM Scope Enforcement
## Summary
Task L.3 starts hardening CRM security around resolved CRM access instead of relying on route-level permissions or legacy role strings alone.
## Implemented in this pass
- Added centralized CRM security helpers in `src/features/crm/security/server/service.ts`
- Added `canViewQuotationPricing()` guard for pricing-bearing quotation outputs
- Applied pricing enforcement to:
- document data
- document preview
- PDF preview
- PDF download
- approved PDF
- approved quotation artifact download
- Switched dashboard revenue visibility to pricing visibility instead of broad quotation-read semantics
- Blocked revenue dashboard export when pricing visibility is missing
- Updated quotation list/detail/create/update/delete routes to pass resolved CRM scope context
- Added quotation service-side branch/product/own-scope enforcement
- Updated approval step actor validation to use resolved CRM role unions
- Added `crm_security_access` audit events for pricing-denied flows
- Added security inventory and boundary documentation
- Added `scripts/security/verify-crm-access.ts`
## Remaining gaps
- Customers and contacts still need full ownership-scope enforcement
- Approval list/detail visibility is still broader than the final resolved-quotation scope
- Team-scope semantics remain approximate until the domain has a first-class team/subordinate model
- Runtime seeded scenario verification is still needed beyond the current static regression script
## Verification
- `npx tsc --noEmit`
- `node --experimental-strip-types scripts/security/verify-crm-access.ts`

View File

@@ -0,0 +1,33 @@
# Task L.3.1: Customer, Contact & Approval Visibility Hardening
## Summary
Task L.3.1 closes the next set of CRM visibility gaps after Task L.3 by moving customer, contact, and approval visibility closer to the resolved CRM access model already used by quotations and dashboard security.
## Implemented
- customer list/detail/update/delete routes now pass resolved CRM scope context
- customer service now filters visibility by:
- branch scope
- ownership scope
- related enquiry/quotation visibility
- marketing monitor visibility through enquiry relationships
- contact routes now inherit resolved customer visibility instead of organization-only reads
- contact access additionally preserves creator visibility
- approval list/detail now require either:
- related quotation visibility
- current approval actor visibility
- admin / organization-wide visibility
- dashboard approval analytics now reuse access-scoped approval listing
- added `customer_scope_violation`, `contact_scope_violation`, and `approval_scope_violation` security audit actions
- expanded static security verification coverage
- documented team-scope and contact-sharing limitations
## Important limitation
The production schema still has no persisted contact-sharing model. That means L.3.1 hardens creator/customer/admin visibility, but it cannot preserve a live shared-contact workflow that does not yet exist in persistence.
## Verification
- `npx tsc --noEmit`
- `node --experimental-strip-types scripts/security/verify-crm-access.ts`

View File

@@ -0,0 +1,58 @@
# Task UX.1: Thai CRM Terminology Standardization
## Files Added
- `docs/business/crm-terminology.md`
- `docs/implementation/task-ux1-thai-crm-terminology-standardization.md`
- `src/features/crm/shared/terminology.ts`
## Files Modified
- `src/config/nav-config.ts`
- `src/features/crm/shared/project-party.ts`
- `src/app/dashboard/crm/page.tsx`
- `src/app/dashboard/crm/customers/page.tsx`
- `src/app/dashboard/crm/leads/page.tsx`
- `src/app/dashboard/crm/leads/[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/approvals/page.tsx`
- `src/app/dashboard/crm/approvals/[id]/page.tsx`
- `src/features/crm/dashboard/components/*`
- `src/app/api/crm/dashboard/export/route.ts`
- `src/features/crm/components/project-parties-editor.tsx`
- `src/features/crm/customers/components/*`
- `src/features/crm/enquiries/components/*`
- `src/features/crm/quotations/components/quotation-columns.tsx`
- `src/features/crm/quotations/components/quotation-document-preview.tsx`
- `src/features/foundation/approval/components/*`
## Terminology Registry
- Added `docs/business/crm-terminology.md` as the frozen source of truth for CRM UI business terms.
## Menu Changes
- Standardized CRM side menu labels to Thai business terminology while keeping `CRM` and `Dashboard` in English.
## Dashboard Changes
- Updated KPI cards, funnel labels, follow-up labels, approval analytics labels, revenue tables, and export labels to the frozen Thai terms.
## Form Changes
- Updated business-facing customer, contact, lead, and enquiry form labels and CTA copy without renaming any data fields.
## PDF Changes
- Updated quotation preview labels to Thai business terminology without changing template mapping keys.
## Report Changes
- Updated CRM dashboard export headers and section labels to Thai business terminology while keeping internal query/filter codes unchanged.
## Remaining Risks
- Some technical or admin-facing screens still intentionally remain English because they are out of scope for this task.
- Master option values seeded in the database may still be English in storage, so this task relies on UI copy and display-layer terminology rather than persistence changes.

View File

@@ -0,0 +1,85 @@
# Task UX.2: CRM Form Design System Standardization
## Objective
Establish one reusable CRM form design foundation for shared controls, field behaviors, filter bars, textarea sizing, and select trigger stability across the CRM surface.
## Files Added
- `docs/implementation/task-ux2-crm-form-design-system-standardization.md`
- `src/features/crm/shared/formats.ts`
- `src/features/crm/components/crm-form-controls.tsx`
- `src/components/forms/fields/currency-field.tsx`
- `src/components/forms/fields/percentage-field.tsx`
## Files Modified
- `docs/standards/project-foundations.md`
- `src/components/forms/fields/date-picker-field.tsx`
- `src/components/forms/fields/index.tsx`
- `src/components/forms/fields/textarea-field.tsx`
- `src/components/ui/select.tsx`
- `src/components/ui/tanstack-form.tsx`
- `src/features/crm/customers/components/contact-form-sheet.tsx`
- `src/features/crm/customers/components/contact-share-sheet.tsx`
- `src/features/crm/customers/components/customer-form-sheet.tsx`
- `src/features/crm/customers/components/customer-owner-card.tsx`
- `src/features/crm/dashboard/components/dashboard-filters.tsx`
- `src/features/crm/enquiries/components/enquiry-form-sheet.tsx`
- `src/features/crm/enquiries/components/enquiry-outcome-card.tsx`
- `src/features/crm/enquiries/components/followup-form-sheet.tsx`
- `src/features/crm/quotations/components/quotation-approval-tab.tsx`
- `src/features/crm/quotations/components/quotation-detail.tsx`
- `src/features/crm/quotations/components/quotation-form-sheet.tsx`
- `src/features/crm/reports/components/report-filter-bar.tsx`
## Drift Inventory
Before this task, CRM forms and filters had multiple inconsistent patterns:
- raw `type="date"` inputs mixed with a calendar button pattern
- numeric commercial fields implemented as generic text or bare number inputs
- long select values stretching trigger width in customer and project-party flows
- arbitrary textarea heights across dialogs, forms, and approval panels
- report and dashboard filters using different control families from CRM forms
- manual quotation dialogs bypassing the shared TanStack field wrappers
## Foundation Created
New reusable CRM form foundation now standardizes:
- date input behavior through `CrmDateInput`
- number, currency, and percentage behavior through `CrmNumberInput`, `CrmCurrencyInput`, and `CrmPercentageInput`
- textarea sizes through `CrmTextarea` with `sm | md | lg`
- display formatting helpers through `formatCrmDate()` and `formatCrmNumber()`
- TanStack form wrappers through `FormCurrencyField` and `FormPercentageField`
- select trigger width stability through the shared `SelectTrigger` primitive
## Module Coverage
The standardization was applied where CRM already had production form or filter surfaces:
- Customers: customer form, contact form, owner assignment dialog, contact sharing dialog
- Enquiries and leads: enquiry form, follow-up form, outcome dialogs
- Quotations: quotation header form, approval submit panel, item/topic/follow-up/attachment dialogs
- Reports and dashboard: shared date filter behavior for dashboard and report filter bars
## UX Decisions Frozen
- Date selection uses a calendar picker
- User-facing date display uses `DD/MM/YYYY`
- Select triggers stay `w-full max-w-full` and truncate long selected labels
- Desktop CRM form layout remains primarily `md:grid-cols-2`
- Textareas use named sizes instead of arbitrary heights
- Report and dashboard filters reuse the same CRM date control family
- Required field display continues to use the existing label `*` indicator pattern
## Verification
- Ran `npm exec tsc --noEmit`
- Verified no remaining raw CRM `type="date"` or ad hoc textarea usage outside the shared CRM controls file itself
## Remaining Notes
- Some older dialogs still use direct `Input` for plain text fields, which is acceptable because the drift for this task was primarily around date, numeric, textarea, and select behavior.
- Several legacy files still contain mojibake in Thai strings. This task avoided rewriting business copy unless required for control standardization.

View File

@@ -0,0 +1,441 @@
# Technical Debt
## After Task D
### Database foreign keys
Customer, contact, and enquiry relationships are still enforced mainly in application logic.
Future:
- add FK constraints after the CRM schema settles
- review cascade behavior before enabling hard relational enforcement
### Date input UX
Several forms still use plain `YYYY-MM-DD` text/date inputs.
Future:
- move to a shared date picker pattern
- standardize timezone handling for CRM forms
### Related enquiry pagination
Customer detail can show related enquiries, but that view still has no dedicated pagination strategy.
Future:
- add paging once real data volume grows
### Follow-up activity timeline
Activity history still relies heavily on audit payloads instead of a curated domain timeline.
Future:
- build a combined activity view/service for CRM interactions
## After Task D.1
### Enquiry Own / Assigned Scope
Current:
sales role ยังอ่าน enquiry ในระดับ organization scope
Future:
เพิ่ม server-side policy:
- sales เห็นเฉพาะ enquiry ที่ตัวเองสร้าง
- หรือ enquiry ที่ assigned ให้ตัวเอง
- sales_manager เห็นทั้งทีม
Priority:
High
### Legacy Business Role Backfill
Current:
ระบบ runtime ใช้ CRM businessRole แล้ว แต่ DB เก่าอาจยังมี role จาก IT-CENTER
Future:
ทำ one-time migration/backfill script สำหรับ memberships.businessRole
Priority:
Medium
## After Task E
### Mixed tax rollup
Quotation items support per-line tax input, but summary calculation still assumes a header-centric tax model.
Future:
- support mixed-tax aggregation from actual item rows
- align document totals and reporting with per-line tax logic
### Attachment storage abstraction
Quotation attachments started as metadata-only and approved PDFs now write to local disk, but there is still no real storage abstraction.
Future:
- introduce a provider abstraction
- support upload/download lifecycle consistently across manual attachments and generated artifacts
- define migration strategy for existing local/public paths
### Revision snapshot coverage
Revision copy currently focuses on quotation header, items, customers, and topics.
Future:
- decide whether follow-ups and attachments belong in revision snapshots
- define immutable vs mutable child-data behavior across revisions
### CRM foreign keys
Core CRM tables still depend on organization-scoped validation in service code.
Future:
- add FK constraints after schema churn slows down
- review delete and soft-delete interactions first
## After Task F
### Permission sync for existing memberships
New permissions are defined in code, but old membership rows may still hold stale permission arrays.
Future:
- add a permission sync/backfill command
- provide admin tooling for role and permission maintenance
### Approval workflow configuration UI
Approval workflows are seeded and usable, but still not configurable from the dashboard.
Future:
- build workflow setup screens
- add step editing and role mapping management
### Approval notifications
Approval flow has no queue-backed notification channel yet.
Future:
- add notification jobs
- support reminders and escalation policy
## After Task G
### Document mapping editor
Template mappings are seeded and readable, but not manageable from UI.
Future:
- add mapping CRUD for admins
- support table column editing
- add draft/publish template workflow
### Product-specific template resolution
Quotation document template resolution still defaults to a coarse product selection strategy.
Future:
- define clearer rules for crane, dock door, solar, and service-specific templates
## After Task H / H.1
### Font compatibility fallback
PDF generation now uses vendored Cordia TTF files from `public/fonts`, which fixed the TTC runtime issue, but generator-side fallback normalization still remains as a safety net.
Future:
- keep validating Thai text metrics against designer output
- remove fallback normalization only after render parity is proven stable
### Local filesystem storage
Approved PDFs currently persist under `public/generated/quotations/<organizationId>/`.
Future:
- replace local public storage for production with object storage
- add signed/private delivery where required
- define backup and cleanup expectations
### Approved artifact immutability
Approved PDF persistence exists, but the lifecycle is not immutable by policy yet.
Future:
- define whether regeneration is allowed after approval
- version approved artifacts explicitly if regeneration must remain possible
- document which snapshot fields are contractual records
### Post-approval automation
Approved PDF generation is still manual by design.
Future:
- add a non-blocking auto-generation hook after final approval
- move heavy generation/retry behavior to a background job when needed
### PDF pipeline test coverage
Task H.1 added dev fixture and HTTP verification scripts, but there is still no CI-backed E2E coverage for the PDF pipeline.
Future:
- promote fixture verification into automated test coverage
- add regression checks for template mappings, storage persistence, and permission enforcement
### Current PDFME template coverage
Current:
- payment, warranty, delivery, and extra topic sections now flow through the dynamic `topic` / `data_topic` engine on page 2 instead of fixed placeholder fields
- approval signature placeholders now resolve through the H.3 signature strategy and `crm_job_title` position lookup
- `quotation_price_data` is now the canonical runtime table input for the price row, while active template JSON still embeds nested `{quotation_price}` and `{currency}` cells inside that table field
Future:
- keep validating pagination and closing-block placement against production designer expectations for large topic sets
- add fixture-backed end-to-end PDF generation verification in CI once a stable runtime path is available
- remove nested price-table placeholders in a controlled template revision once design change windows allow it
- decide whether static template field `line` should remain a tolerated designer-only artifact or be normalized in future audits
### Template version remap operations
Current:
- template version remap is now handled by `npm run seed:pdf-template-version`, not by the base foundation seed
- active `1.1` mappings intentionally preserve historical `1.0` versions for snapshot integrity
Future:
- decide whether template version promotion needs an operator-facing review/approve workflow
- add artifacting or changelog snapshots for template-schema diffs before activation
- decide whether the remap command should write a persistent audit history table instead of JSON-only artifacts
## After Task I
### Storage provider production rollout
Task I introduced a storage provider abstraction with local and S3/MinIO-compatible implementations, but the production rollout still depends on environment configuration and infrastructure ownership outside the app.
Future:
- validate real bucket policies, credentials rotation, and environment separation
- add operational runbooks for provider misconfiguration and storage outage handling
- add smoke checks for startup-time storage provider health
### Legacy approved PDF migration
Task I keeps compatibility for legacy `public/generated/...` approved PDFs, but those legacy files are still outside the new artifact source-of-truth model.
Future:
- add a one-time migration script from legacy local paths into `crm_document_artifacts`
- backfill `approvedArtifactId` where possible
- remove legacy compatibility once migration is complete and verified
### Artifact lifecycle policy depth
Approved artifacts are now immutable-first and locked after generation, but void/regeneration policy is still service-level rather than a full operator workflow.
Future:
- add explicit admin tooling for artifact void and replacement flows
- define whether artifact voiding also clears or supersedes quotation-facing references automatically
- document retention expectations for voided objects in object storage
### Secure artifact delivery model
Approved-PDF viewing now goes through secure server routes and storage-backed reads, but signed-url/offload behavior is not yet used.
Future:
- decide when direct signed URLs are acceptable versus mandatory server streaming
- add response caching strategy for large artifact downloads
- evaluate optional download-audit sampling if volume grows
## After Task J.0
### Missing lifecycle timestamps for KPI precision
Task J.0 freezes KPI rules around `pipelineStage`, but the schema still lacks explicit lifecycle timestamps for stage conversion and closure events.
Future:
- add `convertedToOpportunityAt` for precise lead-to-opportunity timing
- add `closedWonAt` for won-date reporting
- add `closedLostAt` for lost-date reporting
- backfill reporting logic to prefer explicit lifecycle timestamps over inference when available
### Lost reason analytics
Closed-lost semantics are frozen, but there is still no structured `lostReason` field for dashboard grouping and sales analysis.
Future:
- add `lostReason` to the CRM enquiry lifecycle model
- define a controlled vocabulary or master-option category for loss analysis
- expose lost-reason reporting only after the field is reliably captured
### Default customer party role behavior
Customer party definitions are frozen, but there is still no `defaultPartyRole` behavior to help normalize customer selection and reduce operator ambiguity.
Future:
- decide whether customers should carry a suggested default project-party role
- add `defaultPartyRole` only if it improves data-entry quality without creating hidden automation
- keep dashboard logic independent from this field until the model is formally introduced
## After Task D.2.1
### Unified project-party reporting projection
Revenue attribution is now frozen, but the schema still stores enquiry and quotation parties in separate tables rather than a dedicated reporting projection.
Future:
- add a unified reporting view or materialized projection if dashboard/report queries become heavy
- keep quotation-party precedence and enquiry fallback centralized in one service until that projection exists
- avoid duplicating attribution logic in dashboard-specific code
### Historical party snapshot fidelity
Current revenue attribution uses live project-party relationships with quotation-first fallback behavior, but it does not yet provide effective-dated historical party snapshots for reporting.
Future:
- decide whether approved quotation snapshots or another immutable reporting snapshot should become the historical source of truth
- define how reporting should behave if project parties change after quotation approval or after a won/lost outcome
- add backfill or reconciliation tooling if historical party drift becomes a business issue
## After Task J
### Closed-won lifecycle fidelity
Task J ships a live CRM dashboard, but the current schema still lacks a first-class enquiry `closed_won` lifecycle field and dedicated close timestamps.
Current:
- dashboard won-stage metrics currently use quotation status `accepted` as the practical proxy for `closed_won`
Future:
- add explicit enquiry lifecycle support for `closed_won`
- add `closedWonAt` to support precise won-date analytics
- remove dashboard proxy logic once the lifecycle model is formalized
### Follow-up completion semantics
Task J adds follow-up analytics, but the follow-up model still has no explicit completion flag.
Current:
- dashboard `Completed` follow-up metrics infer completion from `outcome` presence
Future:
- add an explicit completion state or activity outcome model for follow-ups
- separate scheduled, completed, cancelled, and skipped follow-up reporting cleanly
### Dashboard own-scope policy
Task J introduces `crm.dashboard.read`, but sales users still view organization-wide KPI data for now.
Future:
- add own-scope dashboard filtering for sales roles
- define whether mixed-role users see own, team, or organization scope by default
- keep exports aligned with the same scope policy once enforced
## After Task L.1
### Deprecated membership.businessRole for CRM authorization
Current:
`memberships.businessRole` is now a compatibility fallback only. Primary CRM authorization moves to `crm_user_role_assignments`.
Future:
- remove CRM authorization dependence on `memberships.businessRole` entirely
- update user-management flows to assign CRM roles through assignment rows instead of membership payloads
- remove the deprecated column only after all CRM modules and admin flows have migrated
## After Task L.2
### Legacy compatibility mirror on membership.businessRole
Current:
User management now writes CRM role assignments directly, but `memberships.businessRole` is still mirrored from the primary CRM role for compatibility with remaining legacy seams.
Future:
- remove mirror writes once every consumer fully resolves CRM access from `crm_user_role_assignments`
- delete the column only after migration tooling, admin screens, and downstream integrations no longer depend on it
### Global project-party filter coverage
Task J adds a global `Project Party Role` filter in the dashboard UI, but the strongest enforcement today is in revenue analytics and export sections.
Future:
- decide which non-revenue widgets should fully honor project-party role filtering
- add a unified filtered reporting projection if cross-widget party-role filtering becomes a hard requirement
## After Task L.3
### Customer and contact ownership enforcement
Current:
Customer/contact modules are still more organization-wide than quotation security and need the same resolved ownership model applied consistently.
Future:
- add resolved CRM access context to every customer/contact service path
- enforce shared-contact visibility without falling back to blanket organization reads
### Approval visibility scoping
Current:
Approval step handling now resolves multi-role actors correctly, but approval list/detail visibility is still broader than final quotation-scoped security.
Future:
- scope approval requests by accessible entity visibility, not organization membership alone
- align dashboard approval widgets and approval detail pages with the same rule
### Team-scope fidelity
Current:
Resolved CRM access supports `team`, but the domain still lacks a first-class subordinate/team graph for precise enforcement.
Future:
- define the canonical team relationship source
- replace current coarse team handling with explicit team membership or manager hierarchy logic
### Contact-sharing persistence gap
Current:
Task C.1 closes the contact-sharing persistence gap, but cross-feature regression coverage for owner/share visibility is still light and team-scope remains approximate.
Future:
- add deeper automated regression coverage for owner/share visibility across customer, lead, enquiry, and quotation flows
- introduce a first-class CRM team hierarchy if the business still needs inherited team visibility

View File

@@ -0,0 +1,39 @@
# CRM Access Enforcement Inventory
## Status Key
- `Protected`: route already passes through resolved CRM access or explicit permission/scope enforcement
- `Partially Protected`: route requires auth/permission but still has legacy or coarse organization-wide behavior
- `Legacy Protected`: route depends on role string or legacy membership behavior instead of resolved union access
- `Not Protected`: no meaningful CRM scope enforcement beyond organization lookup
## Inventory
| Area | Route / Module | Status | Notes |
| --- | --- | --- | --- |
| Customers | `/api/crm/customers` | `Protected` | List/detail/update/delete now pass resolved CRM scope context and customer visibility is filtered by ownership plus related enquiry/quotation signals |
| Contacts | `/api/crm/customers/[id]/contacts` | `Protected` | Contact APIs now inherit customer visibility and creator/admin/team visibility rules from resolved CRM access |
| Leads / Opportunities | `/api/crm/opportunities` and child routes | `Partially Protected` | Uses branch/product/own-scope logic already, but still passes legacy `businessRole` fields through route/service seams |
| Quotations | `/api/crm/quotations` and `[id]` | `Protected` | List/detail/create/update/delete now pass resolved CRM scope context and enforce branch/product/own visibility |
| Quotation pricing surfaces | document data, preview, PDF preview/download, approved PDF | `Protected` | Pricing-bearing outputs now require pricing visibility and emit `crm_security_access` audit events when denied |
| Document artifacts | `/api/crm/document-artifacts/[id]/download` | `Protected` | Approved quotation artifacts now require artifact permission plus pricing visibility guard |
| Approvals | `/api/crm/approvals`, approve/reject/return | `Protected` | Approval list/detail now require related quotation visibility or current-actor/admin visibility; step actor resolution uses resolved CRM access union |
| Dashboard | `/api/crm/dashboard` | `Protected` | Route uses resolved context; revenue widgets are hidden unless quotation pricing is visible |
| Dashboard export | `/api/crm/dashboard/export` | `Protected` | Revenue export explicitly blocked without pricing visibility |
| CRM settings: roles | `/api/crm/settings/roles*` | `Partially Protected` | Permission-gated, but inventory still needs full pass for every mutation route |
| CRM settings: user assignments | `/api/crm/settings/user-role-assignments*` | `Protected` | Explicit permission-gated through CRM role-assignment permissions |
| CRM settings: approval workflows | `/api/crm/settings/approval-workflows*` | `Partially Protected` | Permission-gated at route layer; deeper union-based workflow visibility still pending |
| CRM settings: document templates / sequences | `/api/crm/settings/document-templates*`, `/document-sequences*` | `Partially Protected` | Permission-gated, but not yet audited against the full L.3 export/artifact matrix |
| Master options | `/api/foundation/master-options` | `Partially Protected` | Admin-oriented and permission-gated, but outside resolved CRM scope model |
## High-Risk Findings
1. Team-scope semantics are still coarse because the current model does not yet carry a first-class subordinate/team graph.
2. Contact sharing is now persisted, but team-scope still remains approximate because there is no first-class CRM team hierarchy.
3. Enquiry services already enforce meaningful scope, but several route files still pass legacy role-shaped context instead of a dedicated security context object.
## Follow-up Focus
- Normalize remaining enquiry routes onto the same security-context builder used by quotations and dashboard.
- Add runtime seeded scenario tests once dedicated security fixtures are available.
- Introduce a first-class CRM team hierarchy if the business requires true team-based visibility.

View File

@@ -0,0 +1,114 @@
# CRM Authorization Boundaries
## Source of Truth
Resolved CRM access is the source of truth for CRM authorization.
The effective model is:
1. Authenticated user
2. Active organization membership
3. CRM role assignments
4. Resolved permission union
5. Resolved branch scope
6. Resolved product scope
7. Resolved ownership scope
8. Resolved approval authority
`memberships.businessRole` remains a compatibility fallback only inside the resolver path.
## Enforcement Layers
### Route permission gates
Every CRM route must first require organization access plus at least one explicit permission.
### Service-level scope enforcement
Sensitive list/detail/mutation services must additionally enforce:
- branch scope
- product scope
- ownership visibility
This is required because UI filtering is not a security boundary.
### Pricing visibility
Pricing-bearing outputs require quotation pricing visibility:
- quotation PDF preview/download
- approved PDF
- document preview/data
- revenue dashboard/export
- approved artifact download
Denied pricing access is logged as `crm_security_access` with action `pricing_hidden`.
### Approval authority
Approval step handling must use resolved CRM role unions instead of a single legacy business-role string.
## Current Ownership Model
- `own`: creator or assigned owner/salesman
- `team`: currently treated as broader-than-own, but still requires future team graph formalization
- `organization`: organization-wide inside active tenant
- `monitor`: read-oriented role with limited commercial visibility
## Customer Boundary
Customer visibility is now enforced from resolved CRM access.
- `organization` and admin scopes can see organization customers within branch scope
- `team` currently behaves as branch/product-scoped broad visibility until a first-class team graph exists
- `own` can see customers they created or customers linked to enquiries/quotations they can already access
- `monitor` can see customers only when those customers are related to visible lead/enquiry records
## Contact Boundary
Contact visibility is enforced through customer visibility plus contact ownership rules.
- if a user can access the parent customer, they can access its contact list within their role boundary
- contact creators retain access to their own contacts
- admin/organization scope retains access
- production contact sharing is persisted through `crm_contact_shares`, so explicit share grants are part of the live authorization boundary
## Approval Boundary
Approval visibility is now narrower than generic approval-read permission.
A user can see approval records only when at least one of these is true:
1. they can access the related quotation through resolved CRM scope
2. they are the current approval actor by resolved CRM role
3. they are admin / organization-wide CRM authority
## Artifact Boundary
Artifact permission alone is not enough for approved quotation PDFs.
The caller must satisfy:
1. artifact download permission
2. quotation pricing visibility when the artifact is an approved quotation PDF
## Dashboard Boundary
Dashboard access is split:
- base dashboard visibility: `crm.dashboard.read`
- revenue/commercial visibility: quotation pricing visibility
- approval widgets: approval read permission
- export visibility: same scope as dashboard, with extra revenue export restriction
Approval widgets additionally inherit approval-request visibility rules, not just a raw approval permission.
## Audit Events
Security-sensitive denials are recorded under entity type `crm_security_access` with actions:
- `access_denied`
- `scope_violation`
- `permission_violation`
- `pricing_hidden`

View File

@@ -0,0 +1,41 @@
# Team Scope Limitations
## Current Team Scope Logic
Current CRM authorization resolves the most permissive ownership scope from active CRM role assignments.
For `team` scope today:
- quotation visibility is effectively broader than `own`, but still constrained by branch scope and product scope
- customer visibility is approximated from branch scope plus related enquiry/quotation relationships
- approval visibility inherits quotation visibility or current approval-actor visibility
## Known Limitations
There is not yet a first-class team hierarchy model in the domain.
That means the system does **not** currently know:
- who reports to whom
- which sales users belong to the same explicit team
- whether a manager should see one subset of sales users but not another inside the same branch
Because of that, `team` currently behaves as an approximate operational scope rather than a strict org-chart scope.
## Contact Sharing Status
The production schema now includes persisted contact sharing.
Result:
- contact visibility is enforced through customer ownership, contact creator visibility, and explicit contact-share grants
- cross-owner contact access can now be granted and revoked without relying on demo/mock behavior
## Future Team Hierarchy Model
Recommended future direction:
1. add a first-class manager/team relationship model
2. resolve `team` scope from that relationship instead of permissive approximation
3. keep expanding security fixtures around owner/share behavior once a first-class team graph exists
4. expand runtime security fixtures around manager-team boundaries after real hierarchy data exists

View File

@@ -0,0 +1,160 @@
# Architecture Rules
These rules describe the approved implementation patterns for ALLA OS.
New work should extend these patterns instead of inventing parallel architecture.
## Frontend Architecture
- Use Next.js App Router.
- Prefer server components by default.
- Use client components only for interactivity, browser APIs, or client-side state/query hooks.
- Use `PageContainer` for dashboard page headers and top-level content framing.
- Keep business-facing CRM UI aligned with `docs/business/crm-terminology.md` and `src/features/crm/shared/terminology.ts`.
### Data Loading Pattern
- For data-heavy dashboard/app pages, prefer server prefetch plus `HydrationBoundary`.
- Client tables and detail panes should consume prefetched data with `useSuspenseQuery()`.
- Define query options and query keys in `src/features/<feature>/api/queries.ts`.
- Keep filters in URL state with `nuqs` when the page supports shareable filtering.
Reference examples:
- `src/features/users/components/user-listing.tsx`
- `src/features/crm/customers/components/customer-listing.tsx`
- `src/app/dashboard/crm/settings/user-role-assignments/page.tsx`
### UI Composition Rules
- Reuse shadcn/ui primitives from `src/components/ui/**`.
- Reuse table shell components from `src/components/ui/table/**`.
- Reuse `useDataTable` for table state orchestration.
- Reuse TanStack Form wrappers from `@/components/ui/tanstack-form` and field components from `@/components/forms/fields`.
- Do not create a parallel CRM design system or terminology layer.
## Backend Architecture
- Use Route Handlers under `src/app/api/**` as the HTTP boundary.
- Keep route handlers thin: auth, validation, service call, response mapping, and audit.
- Put business logic in feature or foundation service layers under `src/features/**`.
- Use Drizzle ORM for database access.
- Keep direct database access out of client components.
### Route Structure Pattern
Approved structure:
1. `requireOrganizationAccess()` or another shared auth helper
2. request parsing and schema validation
3. build resolved access/security context when the feature is scope-sensitive
4. call feature/foundation service
5. emit audit/security events through shared audit services
6. return typed JSON/stream response
Reference examples:
- `src/app/api/crm/quotations/route.ts`
- `src/app/api/crm/approvals/route.ts`
- `src/app/api/crm/reports/pipeline/route.ts`
- `src/app/api/crm/reports/export/route.ts`
## Module Structure
Preferred feature layout:
```text
src/features/<feature>/
api/
types.ts
service.ts
queries.ts
mutations.ts
components/
schemas/
server/
```
Rules:
- components import contracts from `api/types.ts`
- UI calls query options from `api/queries.ts`
- UI mutations reuse shared mutation configs from `api/mutations.ts`
- services in `api/service.ts` call route handlers through `src/lib/api-client.ts`
- server business logic stays under feature/foundation server services
## Service Layer Pattern
- Services own business rules, persistence orchestration, and reusable data shaping.
- Services may call other services when extending an existing foundation.
- Services should not depend on UI concerns.
- Report services must use dataset/builders/filter layers instead of embedding ad hoc SQL in routes.
- PDF flows must use the PDF and artifact foundations instead of custom rendering paths.
Reference services:
- `src/features/crm/reports/server/service.ts`
- `src/features/foundation/approval/server/service.ts`
- `src/features/foundation/pdf-generator/server/service.ts`
- `src/features/foundation/storage/service.ts`
## Query Pattern
- Define centralized query key factories in each feature's `api/queries.ts`.
- Keys should group into `all`, `lists()`, `list(filters)`, `details()`, `detail(id)`, and child-resource keys where applicable.
- Server pages prefetch query options and dehydrate.
- Clients read the same keys with `useSuspenseQuery()`.
Reference examples:
- `src/features/products/api/queries.ts`
- `src/features/users/api/queries.ts`
- `src/features/crm/reports/api/queries.ts`
## Mutation Pattern
- Centralize mutation configs in `api/mutations.ts`.
- Shared mutation configs must retain cache invalidation.
- After create: invalidate list-level keys.
- After update: invalidate list and detail keys.
- After delete: invalidate lists and remove stale detail queries.
- Refresh related tabs, counts, previews, and approval panels when they depend on the changed entity.
## Audit Pattern
- Use `auditCreate()`, `auditUpdate()`, `auditDelete()`, or `auditAction()` from `src/features/foundation/audit-log/service.ts`.
- Reuse existing entity types and action naming where possible.
- Security-sensitive denials use `crm_security_access` through `auditCrmSecurityEvent()`.
- Report view/export events use `crm_report`.
Reference examples:
- `src/features/foundation/audit-log/service.ts`
- `src/features/crm/security/server/service.ts`
- `src/features/crm/reports/server/exports/service.ts`
## Security Pattern
- Organization access starts with `requireOrganizationAccess()`.
- CRM authorization must use resolved access, not raw role strings.
- Use `resolveCrmMembershipAccess()` for effective permission/scope unions.
- Use `buildCrmSecurityContext()` for scope-aware CRM services.
- Use `resolveCrmAccess()` / report-context builders for report routes.
- Scope checks must enforce branch, product, ownership, and pricing visibility server-side.
Reference docs:
- `docs/security/crm-authorization-boundaries.md`
- `docs/security/crm-access-enforcement-inventory.md`
- `docs/security/team-scope-limitations.md`
## Explicitly Forbidden
- business logic in route handlers
- client-side only authorization
- direct role-string CRM authorization
- direct DB access from client code
- duplicate export/report/PDF/approval/security foundations
- new Clerk integration
- new SWR or Redux data architecture for app-owned features
- new React Hook Form usage in this repo

View File

@@ -0,0 +1,379 @@
# Project Foundations
This registry is the single source of truth for reusable foundations in ALLA OS.
Review this file before creating new services, routes, exports, scopes, approvals, PDFs, reports, dashboard analytics, CRM follow-up flows, or authorization logic.
If a requested behavior fits one of the foundations below, extend that foundation first. Creating a parallel implementation path requires an explicit rationale in the task contract.
## How To Use This Registry
Review order for non-trivial work:
1. `AGENTS.md`
2. `docs/standards/architecture-rules.md`
3. `docs/standards/ui-ux-rules.md`
4. relevant `docs/adr/**`
5. relevant `docs/business/**`
6. the reusable foundations in this registry
7. existing feature implementations and route handlers that already consume the foundation
For each candidate foundation, answer:
1. Is it reusable for the requested task?
2. Is it actively used by production CRM routes or pages?
3. Is similar logic duplicated elsewhere?
4. Is it governed by one or more ADRs?
5. Will a future AI agent discover it easily from this registry?
If ADR coverage is incomplete, this registry marks the gap explicitly.
## Registry Inventory
### Auth Context Foundation
| Field | Details |
| --- | --- |
| Purpose | Normalize current-user access to app-owned user and session context before downstream foundation or feature logic runs |
| Location | `src/features/foundation/auth-context/**`, `src/lib/auth/session.ts`, `src/auth.ts` |
| Related tasks | Task B, Task D.1, Task L |
| Related ADRs | None dedicated. Governance gap: still governed mainly by `AGENTS.md`, `architecture-rules.md`, and the Auth.js app-owned auth model rather than a dedicated ADR. |
| Reusable services | `getCurrentUser()`, `requireCurrentUser()`, `getCurrentUserContext()` |
| Reusable APIs | No standalone API. Reused by downstream route handlers and services. |
| Reuse status | Reusable and active. Foundation-safe helper layer. |
### Organization Context Foundation
| Field | Details |
| --- | --- |
| Purpose | Resolve active organization and enforce organization-scoped access for app-owned multi-tenant flows |
| Location | `src/features/foundation/organization-context/**`, `src/lib/auth/session.ts` |
| Related tasks | Task B lineage, Tasks C-L |
| Related ADRs | None dedicated. Governance gap: core organization-context rules are implementation-backed but not frozen in a dedicated ADR. |
| Reusable services | `getCurrentOrganization()`, `getActiveOrganizationId()`, `requireOrganizationAccess()` |
| Reusable APIs | No standalone API. Reused by nearly all CRM route handlers. |
| Reuse status | Reusable and active. Mandatory for organization-scoped server work. |
### Permission Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared permission and compatibility role checks outside CRM resolved-access unions |
| Location | `src/features/foundation/permission/**`, `src/lib/auth/rbac.ts` |
| Related tasks | Task B lineage, Task L, Task J.1 |
| Related ADRs | ADR-0014 is related for CRM permissions, but there is no dedicated ADR for the lower-level permission helper layer itself. |
| Reusable services | `hasPermission()`, `requirePermission()`, `hasBusinessRole()` |
| Reusable APIs | No standalone API. Consumed by route handlers and settings surfaces. |
| Reuse status | Reusable and active. Do not replace with route-local permission helpers. |
### Branch Scope Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared branch lookup and branch access validation while branch remains option-backed rather than a first-class domain table |
| Location | `src/features/foundation/branch-scope/**`, `docs/adr/ADR-0006-branch-domain-model.md` |
| Related tasks | Task B, Task B.1, Tasks D-L |
| Related ADRs | ADR-0006 |
| Reusable services | `getUserBranches()`, `getActiveBranch()`, `validateBranchAccess()` |
| Reusable APIs | No standalone API. Used by CRM services and sequence generation flows. |
| Reuse status | Reusable and active. Governance note: branch is still abstraction-backed, so future branch-domain work must preserve compatibility. |
### Audit Foundation
| Field | Details |
| --- | --- |
| Purpose | Central audit persistence and naming pattern for create, update, delete, action, export, and security-denial events |
| Location | `src/features/foundation/audit-log/**`, `tr_audit_logs`, `drizzle/0001_orange_mandarin.sql` |
| Related tasks | Task B lineage and all downstream CRM tasks |
| Related ADRs | None dedicated. Governance gap: audit behavior is widely reused but still lacks a dedicated ADR. |
| Reusable services | `auditAction()`, `auditCreate()`, `auditUpdate()`, `auditDelete()`, `listAuditLogs()` |
| Reusable APIs | No standalone public audit API required for reuse. Consumed from route handlers and service layers. |
| Reuse status | Reusable and active. Mandatory for new CRM mutations, exports, and security denials. |
### Master Data Foundation
| Field | Details |
| --- | --- |
| Purpose | Centralized option and category management for CRM labels, statuses, branches, lost reasons, job titles, project-party roles, report categories, and other governed select data |
| Location | `src/features/foundation/master-options/**`, `src/app/api/foundation/master-options/route.ts`, `ms_options`, `src/db/seeds/foundation.seed.ts` |
| Related tasks | Task B, Task B.1, Hotfix 1, Task D, Task D.4, Task J.1, Task K.1 |
| Related ADRs | ADR-0006, ADR-0012, ADR-0016, ADR-0017 |
| Reusable services | `listMasterOptions()`, `getOptionsByCategory()`, `getActiveOptionsByCategory()` |
| Reusable APIs | `/api/foundation/master-options` |
| Reuse status | Reusable and active. Option hierarchy is already live through `parentId` and is required for categories such as `crm_customer_group` -> `crm_customer_sub_group`. |
| Governed examples | `crm_customer_group`, `crm_customer_sub_group`, `crm_lost_reason`, `crm_job_title`, `crm_project_party_role`, `crm_quotation_type`, `crm_report_category` |
### Document Sequence Foundation
| Field | Details |
| --- | --- |
| Purpose | Centralized document numbering across CRM entities, including preview, reset, branch-aware period scoping, and organization scoping |
| Location | `src/features/foundation/document-sequence/**`, `document_sequences`, `src/app/api/crm/settings/document-sequences/**` |
| Related tasks | Task B, Task B.1, Task D, Task E, Task F, Task J.1 |
| Related ADRs | ADR-0006. Governance gap: no dedicated ADR yet for numbering policy or reset policy. |
| Reusable services | `listDocumentSequences()`, `getDocumentSequence()`, `createDocumentSequence()`, `updateDocumentSequence()`, `resetDocumentSequence()`, `previewDocumentSequenceById()`, `previewNextDocumentCode()`, `generateNextDocumentCode()` |
| Reusable APIs | `/api/crm/settings/document-sequences`, `/api/crm/settings/document-sequences/[id]`, `/api/crm/settings/document-sequences/[id]/preview`, `/api/crm/settings/document-sequences/[id]/reset` |
| Reuse status | Reusable and active. Current production source for customer, contact, opportunity, quotation, and approval codes. |
### Document Template Foundation
| Field | Details |
| --- | --- |
| Purpose | Template metadata, versioning, placeholder mapping, table-column mapping, document resolution, and activation flow for governed document rendering |
| Location | `src/features/foundation/document-template/**`, `src/app/api/crm/document-templates/**`, `src/app/api/crm/settings/document-templates/**`, `crm_document_templates*` tables |
| Related tasks | Task G, Task H, Task H.3, Task H.5, Task J.1 |
| Related ADRs | ADR-0012, ADR-0013 |
| Reusable services | `listDocumentTemplates()`, `getDocumentTemplate()`, `listDocumentTemplateVersions()`, `createDocumentTemplate()`, `updateDocumentTemplate()`, `createDocumentTemplateVersion()`, `updateDocumentTemplateVersion()`, `setDocumentTemplateVersionActive()`, `createDocumentTemplateMapping()`, `updateDocumentTemplateMapping()`, `deleteDocumentTemplateMapping()`, `resolveTemplateForDocument()`, `resolveTemplateMappings()`, `mapDocumentDataToTemplateInput()` |
| Reusable APIs | `/api/crm/document-templates`, `/api/crm/document-templates/[id]`, `/api/crm/document-templates/[id]/versions`, `/api/crm/settings/document-templates`, `/api/crm/settings/document-templates/[id]`, `/api/crm/settings/document-templates/[id]/versions`, `/api/crm/settings/document-templates/versions/[id]`, `/api/crm/settings/document-templates/versions/[id]/mappings`, `/api/crm/settings/document-templates/mappings/[id]` |
| Reuse status | Reusable and active. This is the only approved path for template versions, template mappings, and version activation. |
### Approval Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared approval workflow, step handling, current-actor resolution, request history, and document lifecycle integration |
| Location | `src/features/foundation/approval/**`, `src/app/api/crm/approvals/**`, `src/app/api/crm/settings/approval-workflows/**` |
| Related tasks | Task F, Task H.3, Task J.1, Task L.3, Task L.3.1 |
| Related ADRs | ADR-0007, ADR-0012, ADR-0014 |
| Reusable services | `listApprovalWorkflows()`, `getApprovalWorkflow()`, `createApprovalWorkflow()`, `updateApprovalWorkflow()`, `replaceApprovalWorkflowSteps()`, `listApprovalRequests()`, `getApprovalRequest()`, `getApprovalTimeline()`, `getCurrentApprovalStep()`, `submitForApproval()`, `approveApproval()`, `rejectApproval()`, `returnApproval()`, `cancelApproval()` |
| Reusable APIs | `/api/crm/approvals`, `/api/crm/approvals/[id]`, `/api/crm/approvals/[id]/approve`, `/api/crm/approvals/[id]/reject`, `/api/crm/approvals/[id]/return`, `/api/crm/quotations/[id]/submit-approval`, `/api/crm/settings/approval-workflows`, `/api/crm/settings/approval-workflows/[id]`, `/api/crm/settings/approval-workflows/[id]/steps` |
| Reuse status | Reusable and active. Mandatory for approval workflows and approval analytics. |
### Storage Foundation
| Field | Details |
| --- | --- |
| Purpose | Provider abstraction for local versus S3-compatible object storage plus organization-safe storage key building |
| Location | `src/features/foundation/storage/**`, ADR-0008, ADR-0009 |
| Related tasks | Task I |
| Related ADRs | ADR-0008, ADR-0009 |
| Reusable services | `getStorageProvider()`, `buildOrganizationStorageKey()`, local and S3 provider implementations under `server/` |
| Reusable APIs | No standalone direct upload API yet. Reused through artifact and PDF foundations. |
| Reuse status | Reusable and active. Required for approved-document binary storage. |
### Document Artifact Foundation
| Field | Details |
| --- | --- |
| Purpose | Database-backed artifact metadata, locking, voiding, checksum tracking, and secure download behavior |
| Location | `src/features/foundation/document-artifact/**`, `src/app/api/crm/document-artifacts/[id]/download/route.ts`, `crm_document_artifacts` |
| Related tasks | Task I |
| Related ADRs | ADR-0009 |
| Reusable services | `createArtifact()`, `getArtifact()`, `getActiveArtifactForEntity()`, `lockArtifact()`, `voidArtifact()`, `deleteArtifactMetadataOnly()`, `downloadArtifact()` |
| Reusable APIs | `/api/crm/document-artifacts/[id]/download` |
| Reuse status | Reusable and active. Required for immutable approved-document delivery. |
### PDF Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared PDF generation, preview payload building, dynamic topic expansion, signature resolution, approved snapshot preparation, and approved PDF persistence handoff |
| Location | `src/features/foundation/pdf-generator/**`, `src/features/crm/quotations/document/**`, `docs/business/pdf-mapping-registry.md`, `docs/business/pdf-placeholder-registry.md` |
| Related tasks | Task G, Task H, Task H.1, Task H.2, Task H.3, Task H.5, Task I |
| Related ADRs | ADR-0009, ADR-0012, ADR-0013 |
| Reusable services | `buildQuotationDocumentData()`, `getQuotationDocumentPreviewData()`, `prepareApprovedQuotationSnapshot()`, `resolveQuotationSignatures()`, `getSignaturePositionLookup()`, `generateQuotationPdf()`, `generateApprovedQuotationPdf()`, `getStoredApprovedQuotationPdf()` |
| Reusable APIs | `/api/crm/quotations/[id]/document-data`, `/api/crm/quotations/[id]/document-preview`, `/api/crm/quotations/[id]/pdf-preview`, `/api/crm/quotations/[id]/pdf-download`, `/api/crm/quotations/[id]/approved-pdf` |
| Reuse status | Reusable and active. This is the only approved rendering path for quotation PDFs. |
### Role Management Foundation
| Field | Details |
| --- | --- |
| Purpose | CRM role-profile management, permission matrices, approval authority, scope defaults, and effective-access design-time governance |
| Location | `src/features/foundation/role-management/**`, `src/app/api/crm/settings/roles/**`, `src/lib/auth/rbac.ts` |
| Related tasks | Task L, Task L.2 |
| Related ADRs | ADR-0014 |
| Reusable services | `ensureCrmRoleProfiles()`, `listCrmRoleProfiles()`, `getCrmRoleProfileByCode()`, `updateCrmRoleProfile()`, `cloneCrmRoleProfile()` |
| Reusable APIs | `/api/crm/settings/roles`, `/api/crm/settings/roles/[code]`, `/api/crm/settings/roles/[code]/clone` |
| Reuse status | Reusable and active. Must be extended instead of inventing feature-local permission systems. |
### CRM Role Assignment Foundation
| Field | Details |
| --- | --- |
| Purpose | Multi-role assignment persistence, compatibility backfill, user-level scope unioning, and assignment reference data |
| Location | `src/features/foundation/crm-role-assignments/**`, `src/app/api/crm/settings/user-role-assignments/**`, `src/app/api/users/crm-role-assignment-reference/route.ts` |
| Related tasks | Task L.1, Task L.2 |
| Related ADRs | ADR-0014 |
| Reusable services | `ensureCrmRoleAssignmentsBackfillForMembership()`, `ensureCrmRoleAssignmentsBackfillForOrganization()`, `listResolvedCrmRoleAssignments()`, `createCrmUserRoleAssignment()`, `updateCrmUserRoleAssignment()`, `deleteCrmUserRoleAssignment()`, `listCrmRoleAssignmentsForUser()`, `getRoleAssignmentReferenceSummaries()` |
| Reusable APIs | `/api/crm/settings/user-role-assignments`, `/api/crm/settings/user-role-assignments/[id]`, `/api/users/crm-role-assignment-reference`, `/api/users/[id]/effective-access-preview` |
| Reuse status | Reusable and active. Treat this as part of the authorization foundation, not as a separate ad hoc admin feature. |
### Authorization Foundation
| Field | Details |
| --- | --- |
| Purpose | Resolve CRM permissions, branch scope, product scope, ownership scope, pricing visibility, and approval authority from membership plus CRM role assignments |
| Location | `src/lib/auth/crm-access.ts`, `src/lib/auth/session.ts`, `src/features/crm/security/**`, `docs/security/**` |
| Related tasks | Task L, Task L.1, Task L.2, Task L.3, Task L.3.1 |
| Related ADRs | ADR-0014, ADR-0015, ADR-0017 |
| Reusable services | `resolveCrmMembershipAccess()`, `resolveCrmAccess()`, `buildCrmSecurityContext()`, `canViewQuotationPricing()`, `canAccessScopedCrmRecord()`, `canAccessCustomer()`, `canAccessContact()`, `auditCrmSecurityEvent()` |
| Reusable APIs | Authorization is enforced across CRM route handlers rather than through a standalone API |
| Reuse status | Reusable and active. Mandatory for CRM scope-sensitive server work. |
### Customer Ownership Foundation
| Field | Details |
| --- | --- |
| Purpose | Persistent customer owner assignment, owner history, owner-aware visibility, and owner-based lead suggestion |
| Location | `src/features/crm/customers/**`, `src/app/api/crm/customers/**`, `docs/adr/0015-customer-ownership-contact-sharing.md` |
| Related tasks | Task C, Task C.1, Task L.3.1 |
| Related ADRs | ADR-0015 |
| Reusable services | customer services under `src/features/crm/customers/server/service.ts`, especially owner-aware customer detail, owner mutation, and related customer visibility helpers |
| Reusable APIs | `/api/crm/customers`, `/api/crm/customers/[id]`, `/api/crm/customers/[id]/owner` |
| Reuse status | Reusable and active. Use this before inventing territory or manual owner mirrors. |
### Contact Sharing Foundation
| Field | Details |
| --- | --- |
| Purpose | Persistent contact-level sharing and share-aware contact visibility without mock/demo fallbacks |
| Location | `src/features/crm/customers/**`, `src/app/api/crm/customers/[id]/contacts/[contactId]/shares/**`, `docs/security/crm-authorization-boundaries.md` |
| Related tasks | Task C.1, Task L.3.1 |
| Related ADRs | ADR-0015 |
| Reusable services | contact visibility logic in `src/features/crm/customers/server/service.ts`, `canAccessContact()`, and contact share mutation flows with audit integration |
| Reusable APIs | `/api/crm/customers/[id]/contacts`, `/api/crm/customers/[id]/contacts/[contactId]/shares`, `/api/crm/customers/[id]/contacts/[contactId]/shares/[shareId]` |
| Reuse status | Reusable and active. |
| Governance note | `docs/implementation/task-l31-customer-contact-approval-visibility.md` contains an outdated limitation stating persisted contact sharing does not exist. Treat ADR-0015 and Task C.1 as the current source of truth. |
### Lead Foundation
| Field | Details |
| --- | --- |
| Purpose | Dedicated marketing-owned lead domain, including lead identity, awareness source, marketing follow-up status, lead outcome state, and future `1 lead -> N enquiries` linkage |
| Location | `src/db/schema.ts` (`crm_leads`), `drizzle/0020_lead_foundation_schema.sql`, `drizzle/0021_worried_nebula.sql`, `src/db/seeds/foundation.seed.ts`, `docs/adr/0018-lead-enquiry-domain-separation.md` |
| Related tasks | Task D.5, Task D.5.1 |
| Related ADRs | ADR-0018 |
| Reusable services | None production-ready yet. D.5.1 only stabilizes schema and seed foundations before D.5.2 API work. |
| Reusable APIs | None yet. Lead API surface is explicitly deferred to Task D.5.2. |
| Reuse status | Foundation introduced and partially stabilized. Schema and seed contracts exist, but feature services, APIs, UI, migration flows, and separated datasets are not complete yet. |
| Governance note | This foundation supersedes the old shared lead/enquiry model in ADR-0011. After Task D.5.5.1, active sales execution behavior runs on the renamed `crm_opportunities` implementation while older ADRs and implementation notes remain historical references. |
### Lead / Opportunity Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared opportunity lifecycle with linked lead provenance, assignment governance, related project parties, and follow-up child resources |
| Location | `src/features/crm/opportunities/**`, `src/app/api/crm/opportunities/**`, `docs/adr/0011-lead-enquiry-ownership-model.md`, `docs/implementation/task-d551-force-rename-enquiry-domain-opportunity.md` |
| Related tasks | Task D, Task D.1, Task D.3, Task D.3.1 |
| Related ADRs | ADR-0011, ADR-0014 |
| Reusable services | `getOpportunityReferenceData()`, `listOpportunities()`, `getOpportunityDetail()`, `createOpportunity()`, `updateOpportunity()`, `softDeleteOpportunity()`, `assignOpportunityToUser()`, `reassignOpportunityToUser()`, `listOpportunityProjectParties()` |
| Reusable APIs | `/api/crm/opportunities`, `/api/crm/opportunities/[id]`, `/api/crm/opportunities/[id]/assign`, `/api/crm/opportunities/[id]/reassign`, `/api/crm/opportunities/[id]/customers` |
| Reuse status | Reusable and active. This is the source of truth for the sales opportunity domain. |
| Governance note | Historical ADR/task lineage may still say `enquiry`, but active application code, APIs, and schema were force-renamed to `opportunity` in Task D.5.5.1. |
### Won / Lost Governance Foundation
| Field | Details |
| --- | --- |
| Purpose | Freeze business outcome lifecycle on `crm_opportunities`, including PO capture, lost reason governance, and reopen rules |
| Location | `src/features/crm/opportunities/**`, `src/app/api/crm/opportunities/[id]/mark-won/route.ts`, `src/app/api/crm/opportunities/[id]/mark-lost/route.ts`, `src/app/api/crm/opportunities/[id]/reopen/route.ts` |
| Related tasks | Task D.4, Task J, Task K |
| Related ADRs | ADR-0016 |
| Reusable services | `markOpportunityAsWon()`, `markOpportunityAsLost()`, `reopenLostOpportunity()`, PO attachment helpers under opportunity service |
| Reusable APIs | `/api/crm/opportunities/[id]/mark-won`, `/api/crm/opportunities/[id]/mark-lost`, `/api/crm/opportunities/[id]/reopen`, `/api/crm/opportunities/[id]/po-attachments` |
| Reuse status | Reusable and active. Mandatory for outcome transitions and outcome-driven KPI/report work. |
### Quotation Foundation
| Field | Details |
| --- | --- |
| Purpose | Production quotation CRUD, revision, pricing, item/customer/topic/follow-up child resources, document preview entry points, and approval integration |
| Location | `src/features/crm/quotations/**`, `src/app/api/crm/quotations/**` |
| Related tasks | Task E, Task E.1, Task F, Task G, Task H, Task I |
| Related ADRs | ADR-0007, ADR-0008, ADR-0012, ADR-0013 |
| Reusable services | quotation services under `src/features/crm/quotations/server/service.ts`, quotation document services under `src/features/crm/quotations/document/server/service.ts` |
| Reusable APIs | `/api/crm/quotations`, `/api/crm/quotations/[id]`, `/items`, `/customers`, `/topics`, `/followups`, `/attachments`, `/revisions`, `/submit-approval`, document and PDF child routes |
| Reuse status | Reusable and active. Extend this foundation instead of creating quotation-adjacent custom flows. |
### Revenue Attribution Foundation
| Field | Details |
| --- | --- |
| Purpose | Freeze revenue-owner attribution and project-party analytics so dashboard, reports, and exports share one rule set |
| Location | `src/features/crm/reporting/**`, `src/features/crm/shared/project-party.ts`, dashboard and report services |
| Related tasks | Task D.2, Task D.2.1, Task J, Task K |
| Related ADRs | ADR-0010 |
| Reusable services | `getRevenueByEndCustomer()`, `getRevenueByBillingCustomer()`, `getRevenueByContractor()`, `getRevenueByConsultant()`, `getWonRevenue()`, `getLostRevenue()`, `getLostByReason()`, `getLostByCompetitor()`, `getWinRate()` |
| Reusable APIs | No standalone direct API. Reused by `/api/crm/dashboard`, `/api/crm/dashboard/export`, and `/api/crm/reports/**` routes. |
| Reuse status | Reusable and active. |
| Governance note | The helper layer currently lives under `src/features/crm/reporting/**` while the formal report foundation lives under `src/features/crm/reports/**`. This is a discoverability risk, not a reason to duplicate the analytics logic. |
### Dashboard Foundation
| Field | Details |
| --- | --- |
| Purpose | Operational KPI monitoring, approval widgets, follow-up analytics, sales ranking, revenue analytics, and dashboard export behavior |
| Location | `src/features/crm/dashboard/**`, `src/app/api/crm/dashboard/**` |
| Related tasks | Task J, Task UX.1, Task L.3 |
| Related ADRs | ADR-0010, ADR-0011, ADR-0016 |
| Reusable services | `getCrmDashboardData()` plus its reuse of revenue attribution helpers, approval listing, follow-up aggregation, and pricing-visibility enforcement |
| Reusable APIs | `/api/crm/dashboard`, `/api/crm/dashboard/export` |
| Reuse status | Reusable and active. This is the approved dashboard KPI foundation. |
### CRM Form Design Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared CRM form controls, field wrappers, date and numeric formatting behavior, select trigger sizing, textarea sizing, and filter-bar consistency across CRM pages and dialogs |
| Location | `src/features/crm/components/crm-form-controls.tsx`, `src/features/crm/shared/formats.ts`, `src/components/forms/fields/**`, `src/components/ui/select.tsx`, `src/features/crm/dashboard/components/dashboard-filters.tsx`, `src/features/crm/reports/components/report-filter-bar.tsx` |
| Related tasks | Task UX.1, Task UX.2 |
| Related ADRs | None dedicated. Governance gap: form-system behavior is currently standardized by task lineage and this registry rather than a dedicated ADR. |
| Reusable services | `formatCrmDate()`, `formatCrmNumber()`, `sanitizeDecimalInput()` |
| Reusable components | `CrmDateInput`, `CrmNumberInput`, `CrmCurrencyInput`, `CrmPercentageInput`, `CrmTextarea`, `FormDatePickerField`, `FormCurrencyField`, `FormPercentageField`, `FormTextareaField` |
| Reusable APIs | No standalone API. Reused by CRM forms, dialogs, and filter bars. |
| Reuse status | Reusable and active. Extend this foundation before creating route-local date, number, currency, percentage, textarea, or select-trigger behavior. |
### Follow-Up Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared follow-up tracking across opportunities and quotations, including list/detail child resources, dashboard analytics, and report visibility |
| Location | `src/features/crm/opportunities/**`, `src/features/crm/quotations/**`, `src/features/crm/dashboard/**`, `src/features/crm/reports/server/datasets/pipeline-report.dataset.ts` |
| Related tasks | Task D, Task E, Task J, Task K |
| Related ADRs | ADR-0011, ADR-0016, ADR-0017 |
| Reusable services | `listOpportunityFollowups()`, `createOpportunityFollowup()`, `updateOpportunityFollowup()`, `softDeleteOpportunityFollowup()`, quotation follow-up mutation and query flows under `src/features/crm/quotations/server/service.ts`, dashboard follow-up analytics in `getCrmDashboardData()` |
| Reusable APIs | `/api/crm/opportunities/[id]/followups`, `/api/crm/opportunities/[id]/followups/[followupId]`, `/api/crm/quotations/[id]/followups` |
| Reuse status | Active but fragmented. |
| Governance gap | There is no single shared standalone follow-up server foundation yet. Reuse existing opportunity and quotation follow-up services rather than building a third implementation. |
### Reporting Foundation
| Field | Details |
| --- | --- |
| Purpose | Shared report registry, filter metadata, resolved report context, dataset layer, builder layer, and export helpers |
| Location | `src/features/crm/reports/**`, `src/app/api/crm/reports/**`, `docs/adr/0017-report-foundation.md` |
| Related tasks | Task K.1, Task K.2 |
| Related ADRs | ADR-0010, ADR-0011, ADR-0016, ADR-0017 |
| Reusable services | `buildResolvedReportContext()`, `getCrmReportsResponse()`, `getCrmReportCatalogResponse()`, `getCrmReportFiltersResponse()`, `getCrmPipelineReportResponse()`, `getCrmLeadAgingReportResponse()`, `getCrmOpportunityAgingReportResponse()`, `buildSharedReportFilters()`, export helpers in `server/exports/service.ts` |
| Reusable APIs | `/api/crm/reports`, `/api/crm/reports/catalog`, `/api/crm/reports/filters`, `/api/crm/reports/pipeline`, `/api/crm/reports/lead-aging`, `/api/crm/reports/opportunity-aging`, `/api/crm/reports/export` |
| Reuse status | Reusable and active. Mandatory for new CRM reports and exports. |
## Governance Gaps And Duplication Risks
- `master-options`, `document-sequence`, `document-template`, `dashboard`, and `follow-up` were present in code but missing from the old registry. This document now treats them as first-class foundations.
- The low-level access helpers (`auth-context`, `organization-context`, `permission`, `audit-log`) are production foundations but still lack dedicated ADR coverage.
- `src/features/crm/reporting/**` and `src/features/crm/reports/**` are complementary, but their names are easy to confuse:
- `reporting/**` is the reusable analytics helper layer.
- `reports/**` is the governed report platform and API layer.
- Follow-up behavior is reusable, but the implementation is fragmented across enquiry and quotation modules. Do not create another follow-up subsystem.
- `docs/implementation/task-l31-customer-contact-approval-visibility.md` contains a superseded note about missing persisted contact sharing. Current truth is ADR-0015 plus Task C.1.
- `crm settings` APIs for document templates and sequences are permission-gated, but `docs/security/crm-access-enforcement-inventory.md` still marks them as only partially audited against the full scope matrix.
## Foundations That Must Be Checked By Topic
- Approvals: Approval Foundation, Authorization Foundation, Audit Foundation, PDF Foundation
- CRM leads and enquiries: Lead / Enquiry Foundation, Won / Lost Governance Foundation, Follow-Up Foundation, Authorization Foundation
- CRM lead split-domain migration: Lead Foundation, Lead / Enquiry Foundation, Won / Lost Governance Foundation, Authorization Foundation, Reporting Foundation, Dashboard Foundation
- Customers and contacts: Customer Ownership Foundation, Contact Sharing Foundation, Authorization Foundation
- Dashboard analytics: Dashboard Foundation, Revenue Attribution Foundation, Follow-Up Foundation, Reporting Foundation
- Forms and filters: CRM Form Design Foundation, Master Data Foundation
- PDFs and documents: Document Template Foundation, PDF Foundation, Storage Foundation, Document Artifact Foundation
- Reports and exports: Reporting Foundation, Revenue Attribution Foundation, Authorization Foundation, Audit Foundation
- Settings and admin configuration: Master Data Foundation, Document Sequence Foundation, Document Template Foundation, Approval Foundation, Role Management Foundation, CRM Role Assignment Foundation
## Registry Outcome
- ALLA OS Foundation Registry = COMPLETE
- ALLA OS Reuse Discovery = READY
- ALLA OS Governance Quality = IMPROVED

View File

@@ -0,0 +1,666 @@
# Task Catalog
This catalog is the official index of completed ALLA OS task history.
Use it to answer these questions before implementation:
- Which task created this feature or foundation?
- Which ADR governs this area?
- Which foundation was created or modified?
- Which implementation note should be reviewed first?
This file complements:
- `AGENTS.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/**`
## How To Use This Catalog
Before changing any feature, foundation, report, PDF flow, or authorization behavior:
1. Identify the related foundation in `project-foundations.md`.
2. Find the creating and modifying tasks in this catalog.
3. Review the linked ADRs.
4. Review the listed implementation notes before coding.
5. Reuse the established pattern before creating a new one.
## Foundation -> Task Map
| Foundation | Created By | Key Follow-up Tasks |
| --- | --- | --- |
| Auth Context Foundation | Task B | Task B.1, Task D.1 |
| Organization Context Foundation | Task B | Task B.1 |
| Permission Foundation | Task B | Task L, Task J.1 |
| Branch Scope Foundation | Task B | Task B.1, Task L, Task L.3 |
| Audit Foundation | Task B | Tasks C-I, K.1, K.2, L.3 |
| Master Data Foundation | Task B | Task B.1, Task D.4, Task J.1, Task K.1, Hotfix 1 |
| Document Sequence Foundation | Task B | Task B.1, Task D, Task E, Task J.1 |
| Customer Ownership Foundation | Task C.1 | Task L.3.1 |
| Contact Sharing Foundation | Task C.1 | Task L.3.1 |
| Approval Foundation | Task F | Task H.3, Task J.1, Task L.3, Task L.3.1 |
| Document Template Foundation | Task G | Task H.2, Task H.3, Task H.5, Task H.5.3, Task J.1 |
| PDF Foundation | Task H | Task H.1, Task H.2, Task H.3, Task H.5, Task H.5.1, Task H.5.3, Task I |
| Document Artifact Foundation | Task I | Task L.3 |
| Storage Foundation | Task I | Task L.3 |
| Dashboard Foundation | Task J | Task D.3, Task D.4, Task J.0, Task L.3, UX.1 |
| Reporting Foundation | Task K.1 | Task K.2 |
| Role Management Foundation | Task L | Task L.1, Task L.2 |
| CRM Role Assignment Foundation | Task L.1 | Task L.2 |
| Authorization Foundation | Task L | Task L.1, Task L.2, Task L.3, Task L.3.1 |
| Lead Foundation | Task D.5 | Task D.5.1, Task D.5.2 |
| Follow-Up Foundation | Task D plus Task E | Task J, Task K.2 |
| Revenue Attribution Foundation | Task D.2.1 | Task J, Task K.1, Task K.2 |
## ADR -> Task Map
| ADR | Related Tasks |
| --- | --- |
| ADR-0006 Branch Domain Model | Task B, Task B.1, Task J.1 |
| ADR-0007 Quotation Revision Strategy | Task E.1, Task F |
| ADR-0008 Attachment Storage Strategy | Task E.1, Task I |
| ADR-0009 Approved Document Storage Lifecycle | Task H.1, Task I |
| ADR-0010 Revenue Attribution Governance | Task D.2.1, Task J, Task K.1, Task K.2 |
| ADR-0011 Lead and Enquiry Ownership Model | Task D.3, Task D.3.1, Task J, Task K.2 |
| ADR-0012 Approval Signature Strategy | Task H.3, Task J.1 |
| ADR-0013 PDF Visual Parity Strategy | Task H.5, Task H.5.1, Task H.5.3 |
| ADR-0014 CRM Multi-Role User Assignment | Task L, Task L.1, Task L.2, Task L.3 |
| ADR-0015 Customer Ownership and Contact Sharing | Task C.1, Task L.3.1 |
| ADR-0016 Won / Lost Lifecycle Governance | Task D.4, Task J, Task K.2 |
| ADR-0017 CRM Report Foundation | Task K.1, Task K.2 |
| ADR-0018 Lead / Enquiry Domain Separation | Task D.5, Task D.5.1, Task D.5.2 |
## Task Dependency Map
- Task B depends on: Task A
- Task B.1 depends on: Task B
- Task C depends on: Task B, Task B.1
- Task C.1 depends on: Task C, Task L.3 context for later security hardening
- Task D depends on: Task B, Task B.1, Task C
- Task D.1 depends on: Task D, Task B foundations
- Task D.2 depends on: Task D, Task E
- Task D.2.1 depends on: Task D.2, Task J.0
- Task D.3 depends on: Task D, Task D.1
- Task D.3.1 depends on: Task D.3
- Task D.4 depends on: Task D, Task D.3
- Task D.5 depends on: Task D.3, Task D.4, Task J, Task K.2, Task L.3
- Task D.5.1 depends on: Task D.5
- Task D.5.2 depends on: Task D.5.1
- Task E depends on: Task D, Task B foundations
- Task E.1 depends on: Task E
- Task F depends on: Task E, Task E.1
- Task G depends on: Task E, Task F
- Task H depends on: Task G, Task F
- Task H.1 depends on: Task H
- Task H.2 depends on: Task H, Task H.1
- Task H.3 depends on: Task F, Task H.2
- Task H.5 depends on: Task H.2, Task H.3
- Task H.5.1 depends on: Task H.5
- Task H.5.2 depends on: historical record missing
- Task H.5.3 depends on: Task H.5, Task H.5.1
- Task I depends on: Task H, Task H.1
- Task J.0 depends on: Task D.2
- Task J depends on: Task J.0, Task D.2.1, Task D.3, Task D.4, Task F
- Task J.1 depends on: Task B, Task F, Task G
- Task K.1 depends on: Task J, Task L foundations
- Task K.2 depends on: Task K.1, Task D.3, Task D.4, Task L.3
- Task L depends on: Task D.1, Task J
- Task L.1 depends on: Task L
- Task L.2 depends on: Task L.1, users foundation
- Task L.3 depends on: Task L, Task L.1, Task I, Task J
- Task L.3.1 depends on: Task C.1, Task L.3, Task F
- Task UX.1 depends on: Task D.3, Task J
- Hotfix 1 depends on: Task C, Task B master options
## Task Entries
### Task A
- Task ID: `Task A`
- Task Name: `Template Audit`
- Status: `Completed`
- Objective: Freeze the initial project conventions and identify which repo areas were app-owned versus template/demo seams before CRM production work began.
- Related ADRs: `None dedicated`
- Foundations Created: `None`
- Foundations Modified: `None`
- Major Deliverables: project structure audit, tenant-boundary freeze, module-pattern guidance, `Layout.md` usage guidance, migrated-pattern references.
- Key Files: `docs/implementation/task-a-template-audit.md`
- Read Before Modify: `task-a-template-audit.md`, `AGENTS.md`, `architecture-rules.md`
### Task B
- Task ID: `Task B`
- Task Name: `Template Audit`
- Status: `Completed`
- Objective: Create the shared foundation layer without introducing CRM business modules.
- Related ADRs: `None dedicated`
- Foundations Created: `Auth Context Foundation`, `Organization Context Foundation`, `Permission Foundation`, `Branch Scope Foundation`, `Master Data Foundation`, `Document Sequence Foundation`, `Audit Foundation`
- Foundations Modified: `None`
- Major Deliverables: schema and migration for early foundations, master-options API, shared helpers, audit helper, initial master-options UI path.
- Key Files: `docs/implementation/task-b-template-audit.md`, `src/features/foundation/**`, `src/app/api/foundation/master-options/route.ts`
- Read Before Modify: `task-b-template-audit.md`, `project-foundations.md`, `task-b1-foundation-stabilization.md`
### Task B.1
- Task ID: `Task B.1`
- Task Name: `Foundation Stabilization`
- Status: `Completed`
- Objective: Stabilize foundation seed data and isolate production CRM routes from demo seams.
- Related ADRs: `ADR-0006`
- Foundations Created: `None`
- Foundations Modified: `Master Data Foundation`, `Document Sequence Foundation`, `Branch Scope Foundation`, `Auth Context Foundation`, `Organization Context Foundation`, `Permission Foundation`
- Major Deliverables: repeatable foundation seed, production-safe CRM routes, demo isolation, schema and service review notes.
- Key Files: `docs/implementation/task-b1-foundation-stabilization.md`, `src/db/seeds/foundation.seed.ts`, `src/app/dashboard/crm/**`
- Read Before Modify: `task-b1-foundation-stabilization.md`, `task-b-template-audit.md`, `project-foundations.md`
### Task C
- Task ID: `Task C`
- Task Name: `Customer + Contact Production Module`
- Status: `Completed`
- Objective: Deliver the first production customer and contact CRUD module on top of the shared foundations.
- Related ADRs: `None dedicated`
- Foundations Created: `None dedicated`
- Foundations Modified: `Audit Foundation`, `Master Data Foundation`, `Document Sequence Foundation`
- Major Deliverables: `crm_customers`, `crm_customer_contacts`, customer and contact APIs, production list/detail UI, audit integration, search/filter plumbing.
- Key Files: `docs/implementation/task-c-customer-contact.md`, `src/features/crm/customers/**`, `src/app/api/crm/customers/**`
- Read Before Modify: `task-c-customer-contact.md`, `project-foundations.md`, `task-c1-customer-ownership-contact-sharing-governance.md`, `task-l31-customer-contact-approval-visibility.md`
### Task C.1
- Task ID: `Task C.1`
- Task Name: `Customer Ownership and Contact Sharing Governance`
- Status: `Completed`
- Objective: Add persistent customer ownership and persistent contact sharing to close CRM visibility and routing gaps.
- Related ADRs: `ADR-0015`
- Foundations Created: `Customer Ownership Foundation`, `Contact Sharing Foundation`
- Foundations Modified: `Authorization Foundation`, `Lead / Enquiry Foundation`
- Major Deliverables: owner fields and history, `crm_contact_shares`, owner/share APIs, owner UI, contact sharing UI, lead owner suggestion behavior, audit events.
- Key Files: `docs/implementation/task-c1-customer-ownership-contact-sharing-governance.md`, `docs/adr/0015-customer-ownership-contact-sharing.md`, `src/app/api/crm/customers/[id]/owner/route.ts`, `src/app/api/crm/customers/[id]/contacts/[contactId]/shares/**`
- Read Before Modify: `0015-customer-ownership-contact-sharing.md`, `task-c1-customer-ownership-contact-sharing-governance.md`, `project-foundations.md`, `crm-authorization-boundaries.md`
### Task D
- Task ID: `Task D`
- Task Name: `Enquiry Production Module`
- Status: `Completed`
- Objective: Deliver the production enquiry module and the first live follow-up flows.
- Related ADRs: `None dedicated`
- Foundations Created: `Follow-Up Foundation` (enquiry slice)
- Foundations Modified: `Document Sequence Foundation`, `Audit Foundation`, `Master Data Foundation`
- Major Deliverables: `crm_enquiries`, `crm_enquiry_followups`, enquiry CRUD APIs, follow-up APIs, enquiry detail UI, customer integration, sequence generation.
- Key Files: `docs/implementation/task-d-enquiry-production.md`, `src/features/crm/enquiries/**`, `src/app/api/crm/enquiries/**`
- Read Before Modify: `task-d-enquiry-production.md`, `project-foundations.md`, `task-d1-enquiry-assignment-role-stabilization.md`, `task-d3-lead-enquiry-ownership-refinement.md`, `task-d4-won-lost-lifecycle-governance.md`
### Task D.1
- Task ID: `Task D.1`
- Task Name: `Enquiry Assignment and CRM Role Stabilization`
- Status: `Completed`
- Objective: Stabilize assignment behavior and early CRM role handling around enquiries.
- Related ADRs: `None dedicated`
- Foundations Created: `None`
- Foundations Modified: `Auth Context Foundation`, `Permission Foundation`, early `Authorization Foundation` lineage
- Major Deliverables: assignment routes, assignment permissions, auth-context refinements, UI assignment support, audit integration.
- Key Files: `docs/implementation/task-d1-enquiry-assignment-role-stabilization.md`, `src/features/foundation/auth-context/service.ts`, `src/app/api/crm/enquiries/[id]/assign/route.ts`
- Read Before Modify: `task-d1-enquiry-assignment-role-stabilization.md`, `task-d-enquiry-production.md`, `task-l-crm-permission-role-management.md`
### Task D.2
- Task ID: `Task D.2`
- Task Name: `Billing Customer + Project Parties`
- Status: `Completed`
- Objective: Freeze separate billing-customer and project-party modeling for enquiries and quotations.
- Related ADRs: `None dedicated`
- Foundations Created: `None dedicated`
- Foundations Modified: `Revenue Attribution Foundation` lineage, `Master Data Foundation`, CRM shared project-party usage
- Major Deliverables: `crm_enquiry_customers`, quotation party remark support, shared `crm_project_party_role` sourcing, synchronized project-party UI.
- Key Files: `docs/implementation/task-d2-billing-customer-project-parties.md`, `src/features/crm/components/project-parties-editor.tsx`, `src/features/crm/shared/project-party.ts`
- Read Before Modify: `task-d2-billing-customer-project-parties.md`, `task-d21-revenue-attribution-governance.md`, `task-j0-kpi-definition-freeze.md`
### Task D.2.1
- Task ID: `Task D.2.1`
- Task Name: `Revenue Attribution and Party Governance`
- Status: `Completed`
- Objective: Freeze revenue-owner attribution and relationship analytics before dashboard and report expansion.
- Related ADRs: `ADR-0010`
- Foundations Created: `Revenue Attribution Foundation`
- Foundations Modified: `Dashboard Foundation`, `Reporting Foundation` lineage
- Major Deliverables: grouped revenue helpers, project-party governance, fallback rules, ADR freeze for reporting attribution.
- Key Files: `docs/implementation/task-d21-revenue-attribution-governance.md`, `docs/adr/0010-revenue-attribution-governance.md`, `src/features/crm/reporting/server/service.ts`
- Read Before Modify: `0010-revenue-attribution-governance.md`, `task-d21-revenue-attribution-governance.md`, `task-j-crm-dashboard-kpi.md`, `task-k1-report-foundation.md`
### Task D.3
- Task ID: `Task D.3`
- Task Name: `Lead / Enquiry Ownership Refinement`
- Status: `Completed`
- Objective: Freeze the single-record lead/enquiry lifecycle, ownership, visibility, and KPI language.
- Related ADRs: `ADR-0011`
- Foundations Created: `Lead / Enquiry Foundation`
- Foundations Modified: `Dashboard Foundation`, `Authorization Foundation`
- Major Deliverables: `pipelineStage`, marketing role, lead/enquiry visibility rules, KPI wording changes, ADR freeze.
- Key Files: `docs/implementation/task-d3-lead-enquiry-ownership-refinement.md`, `docs/adr/0011-lead-enquiry-ownership-model.md`, `src/features/crm/enquiries/server/service.ts`
- Read Before Modify: `0011-lead-enquiry-ownership-model.md`, `task-d3-lead-enquiry-ownership-refinement.md`, `task-d31-leads-enquiries-navigation-separation.md`, `task-k2-pipeline-reports.md`
### Task D.3.1
- Task ID: `Task D.3.1`
- Task Name: `Leads / Enquiries Navigation Separation`
- Status: `Completed`
- Objective: Split lead and enquiry into separate UX workspaces while preserving one shared backend model.
- Related ADRs: `ADR-0011`
- Foundations Created: `None`
- Foundations Modified: `Lead / Enquiry Foundation`, `Dashboard Foundation`
- Major Deliverables: `/dashboard/crm/leads`, workspace-specific query invalidation, lead permissions, ADR clarification for navigation separation.
- Key Files: `docs/implementation/task-d31-leads-enquiries-navigation-separation.md`, `src/app/dashboard/crm/leads/**`, `src/features/crm/enquiries/**`
- Read Before Modify: `0011-lead-enquiry-ownership-model.md`, `task-d31-leads-enquiries-navigation-separation.md`, `task-d3-lead-enquiry-ownership-refinement.md`
### Task D.4
- Task ID: `Task D.4`
- Task Name: `Won / Lost Lifecycle Governance`
- Status: `Completed`
- Objective: Freeze official won/lost outcome behavior on enquiries rather than deriving it from quotation status.
- Related ADRs: `ADR-0016`
- Foundations Created: `Won / Lost Governance Foundation`
- Foundations Modified: `Dashboard Foundation`, `Reporting Foundation`, `Master Data Foundation`
- Major Deliverables: won/lost outcome fields, PO attachments, lost reason master data, mark-won/mark-lost/reopen APIs, outcome card, reporting helpers.
- Key Files: `docs/implementation/task-d4-won-lost-lifecycle-governance.md`, `docs/adr/0016-won-lost-lifecycle-governance.md`, `src/app/api/crm/enquiries/[id]/mark-won/route.ts`
- Read Before Modify: `0016-won-lost-lifecycle-governance.md`, `task-d4-won-lost-lifecycle-governance.md`, `task-j-crm-dashboard-kpi.md`, `task-k2-pipeline-reports.md`
### Task D.5
- Task ID: `Task D.5`
- Task Name: `Lead / Enquiry Domain Separation`
- Status: `In progress`
- Objective: Replace the single-record lead/enquiry persistence model with a split-domain direction built around `crm_leads` plus lead-linked enquiries.
- Related ADRs: `ADR-0018`
- Foundations Created: `Lead Foundation`
- Foundations Modified: `Lead / Enquiry Foundation` lineage, `Document Sequence Foundation`, `Master Data Foundation`
- Major Deliverables: ADR-0018 target direction, `crm_leads`, `crm_enquiries.lead_id`, lead option seeds, lead/enquiry split foundation planning.
- Key Files: `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`
- Read Before Modify: `0018-lead-enquiry-domain-separation.md`, `task-d5-lead-enquiry-domain-separation.md`, `0011-lead-enquiry-ownership-model.md`, `task-d31-leads-enquiries-navigation-separation.md`, `task-k2-pipeline-reports.md`
### Task D.5.1
- Task ID: `Task D.5.1`
- Task Name: `Lead Foundation Stabilization & Output Completion`
- Status: `Completed`
- Objective: Audit the existing D.5 phase-1 foundation work, avoid duplicate schema creation, complete missing registrations, and document compatibility status before lead API work begins.
- Related ADRs: `ADR-0018`
- Foundations Created: `None`
- Foundations Modified: `Lead Foundation`, `Master Data Foundation` registration, `Document Sequence Foundation` audit visibility
- Major Deliverables: lead foundation audit, master-option category contract completion, foundation registry registration, task catalog registration, D.5.1 implementation note.
- Key Files: `docs/implementation/task-d51-lead-foundation-stabilization.md`, `docs/standards/project-foundations.md`, `docs/standards/task-catalog.md`, `src/features/foundation/master-options/types.ts`
- Read Before Modify: `task-d51-lead-foundation-stabilization.md`, `0018-lead-enquiry-domain-separation.md`, `task-d5-lead-enquiry-domain-separation.md`, `src/db/schema.ts`, `src/db/seeds/foundation.seed.ts`
### Task D.5.2
- Task ID: `Task D.5.2`
- Task Name: `Lead Domain Service and API Foundation`
- Status: `Completed`
- Objective: Create the first production lead service and API slice on top of `crm_leads` while preserving legacy enquiry behavior.
- Related ADRs: `ADR-0018`
- Foundations Created: `None`
- Foundations Modified: `Lead Foundation`, `Audit Foundation`, `Document Sequence Foundation`, `Authorization Foundation` reuse path
- Major Deliverables: lead CRUD service, `/api/crm/leads/**`, lead follow-up API backed by audit events, lead reference-data integration, customer-owner suggestion output.
- Key Files: `docs/implementation/task-d52-lead-domain-service-api-foundation.md`, `src/features/crm/leads/**`, `src/app/api/crm/leads/**`
- Read Before Modify: `task-d52-lead-domain-service-api-foundation.md`, `task-d51-lead-foundation-stabilization.md`, `0018-lead-enquiry-domain-separation.md`, `task-d5-lead-enquiry-domain-separation.md`
### Task E
- Task ID: `Task E`
- Task Name: `Quotation Production Module`
- Status: `Completed`
- Objective: Deliver the production quotation module and its core child-resource model.
- Related ADRs: `None dedicated at creation time`
- Foundations Created: `Quotation Foundation`
- Foundations Modified: `Document Sequence Foundation`, `Audit Foundation`, `Follow-Up Foundation`
- Major Deliverables: quotation CRUD, items, customers/project parties, topics, follow-ups, attachments metadata, revisions, detail UI, customer/enquiry linkage.
- Key Files: `docs/implementation/task-e-quotation-production.md`, `src/features/crm/quotations/**`, `src/app/api/crm/quotations/**`
- Read Before Modify: `task-e-quotation-production.md`, `task-e1-quotation-stabilization.md`, `0007-quotation-revision-strategy.md`, `0008-attachment-storage-strategy.md`
### Task E.1
- Task ID: `Task E.1`
- Task Name: `Quotation Stabilization Before Approval`
- Status: `Completed`
- Objective: Freeze quotation revision and attachment assumptions before approval work begins.
- Related ADRs: `ADR-0007`, `ADR-0008`
- Foundations Created: `None`
- Foundations Modified: `Quotation Foundation`, `Master Data Foundation`
- Major Deliverables: status vocabulary expansion, revision guard, quotation-related master options, approval readiness rules, ADRs for revision and attachment strategy.
- Key Files: `docs/implementation/task-e1-quotation-stabilization.md`, `docs/adr/0007-quotation-revision-strategy.md`, `docs/adr/0008-attachment-storage-strategy.md`
- Read Before Modify: `task-e1-quotation-stabilization.md`, `0007-quotation-revision-strategy.md`, `0008-attachment-storage-strategy.md`, `task-f-approval-production.md`
### Task F
- Task ID: `Task F`
- Task Name: `Approval Production`
- Status: `Completed`
- Objective: Introduce the first production approval workflow on top of quotation flows and keep it generic for future documents.
- Related ADRs: `ADR-0007`
- Foundations Created: `Approval Foundation`
- Foundations Modified: `Quotation Foundation`, `Audit Foundation`
- Major Deliverables: approval persistence, approval APIs, approval UI, quotation approval submission, seeded workflow steps, approval permissions.
- Key Files: `docs/implementation/task-f-approval-production.md`, `src/features/foundation/approval/**`, `src/app/api/crm/approvals/**`
- Read Before Modify: `task-f-approval-production.md`, `project-foundations.md`, `task-h3-approval-signature-strategy.md`, `task-l31-customer-contact-approval-visibility.md`
### Task G
- Task ID: `Task G`
- Task Name: `Quotation Document Preview Foundation`
- Status: `Completed`
- Objective: Add governed document templates and a production document-preview path before binary PDF generation.
- Related ADRs: `None dedicated at creation time`
- Foundations Created: `Document Template Foundation`
- Foundations Modified: `Quotation Foundation`
- Major Deliverables: template schema tables, template APIs, template settings UI, mapping resolver, preview APIs, approved snapshot preparation.
- Key Files: `docs/implementation/task-g-document-preview-foundation.md`, `src/features/foundation/document-template/**`, `src/features/crm/quotations/document/**`
- Read Before Modify: `task-g-document-preview-foundation.md`, `task-h-pdf-generation-persistence.md`, `task-h53-pdf-template-version-remap.md`
### Task H
- Task ID: `Task H`
- Task Name: `PDF Generation and Approved Document Persistence`
- Status: `Completed`
- Objective: Add server-side PDF generation and the first approved-document persistence path.
- Related ADRs: `None dedicated at creation time`
- Foundations Created: `PDF Foundation`
- Foundations Modified: `Quotation Foundation`, `Approval Foundation`
- Major Deliverables: pdfme generation service, preview/download/approved PDF APIs, approved snapshot fields, local-file persistence, PDF permissions.
- Key Files: `docs/implementation/task-h-pdf-generation-persistence.md`, `src/features/foundation/pdf-generator/server/service.ts`, `src/app/api/crm/quotations/[id]/pdf-preview/route.ts`
- Read Before Modify: `task-h-pdf-generation-persistence.md`, `task-h1-pdf-generation-stabilization.md`, `task-i-storage-artifact-lifecycle.md`
### Task H.1
- Task ID: `Task H.1`
- Task Name: `PDF Generation Stabilization`
- Status: `Completed`
- Objective: Validate and harden the Task H PDF flow with fixtures, verification scripts, and storage-lifecycle clarification.
- Related ADRs: `ADR-0009`
- Foundations Created: `None`
- Foundations Modified: `PDF Foundation`
- Major Deliverables: repeatable fixture data, verification script, persistence checks, lifecycle ADR, debt documentation.
- Key Files: `docs/implementation/task-h1-pdf-generation-stabilization.md`, `docs/adr/0009-approved-document-storage-lifecycle.md`, `scripts/verify-task-h1.js`
- Read Before Modify: `task-h1-pdf-generation-stabilization.md`, `0009-approved-document-storage-lifecycle.md`, `task-i-storage-artifact-lifecycle.md`
### Task H.2
- Task ID: `Task H.2`
- Task Name: `PDFME Dynamic Topic Engine + Mapping Hotfix`
- Status: `Completed`
- Objective: Replace brittle static topic mappings with a dynamic runtime topic engine and fix PDF data transforms.
- Related ADRs: `None dedicated`
- Foundations Created: `Dynamic topic engine within PDF Foundation`
- Foundations Modified: `PDF Foundation`, `Document Template Foundation`
- Major Deliverables: runtime topic engine, PDF transforms, product-topic mapping, mapping cleanup, preview payload hardening.
- Key Files: `docs/implementation/task-h2-pdfme-mapping-transform-hotfix.md`, `src/features/crm/quotations/document/server/pdf-topic-engine.ts`, `src/features/crm/quotations/document/server/pdfme-transforms.ts`
- Read Before Modify: `task-h2-pdfme-mapping-transform-hotfix.md`, `task-h5-pdf-mapping-audit-visual-parity.md`, `pdf-mapping-registry.md`, `pdf-placeholder-registry.md`
### Task H.3
- Task ID: `Task H.3`
- Task Name: `Approval Signature Strategy`
- Status: `Completed`
- Objective: Resolve prepared-by, approved-by, and authorized-by signatures from live approval and membership data.
- Related ADRs: `ADR-0012`
- Foundations Created: `Signature strategy within PDF Foundation`
- Foundations Modified: `PDF Foundation`, `Approval Foundation`, `Master Data Foundation`
- Major Deliverables: signature resolver, position lookup, mapping updates for `app1/app2/app3`, snapshot signature persistence, preview signature section.
- Key Files: `docs/implementation/task-h3-approval-signature-strategy.md`, `docs/adr/0012-approval-signature-strategy.md`, `src/features/crm/quotations/document/server/signature-resolver.ts`
- Read Before Modify: `0012-approval-signature-strategy.md`, `task-h3-approval-signature-strategy.md`, `pdf-mapping-registry.md`
### Task H.5
- Task ID: `Task H.5`
- Task Name: `PDF Mapping Audit & Visual Parity`
- Status: `Completed`
- Objective: Establish an auditable PDF mapping and parity review process for runtime payloads and templates.
- Related ADRs: `ADR-0013`
- Foundations Created: `PDF audit layer within PDF Foundation`
- Foundations Modified: `PDF Foundation`, `Document Template Foundation`
- Major Deliverables: audit scripts, fixture strategy, parity checklist, mapping registry, visual parity ADR.
- Key Files: `docs/implementation/task-h5-pdf-mapping-audit-visual-parity.md`, `docs/adr/0013-pdf-visual-parity-strategy.md`, `docs/implementation/pdf-parity-checklist.md`
- Read Before Modify: `0013-pdf-visual-parity-strategy.md`, `task-h5-pdf-mapping-audit-visual-parity.md`, `pdf-mapping-registry.md`, `pdf-placeholder-registry.md`
### Task H.5.1
- Task ID: `Task H.5.1`
- Task Name: `PDF Integrity Audit & Payload Parity`
- Status: `Completed`
- Objective: Extend H.5 into integrity checks for active versions, placeholders, payload hashes, and approved snapshot parity.
- Related ADRs: `ADR-0013`
- Foundations Created: `None`
- Foundations Modified: `PDF Foundation`, `Document Template Foundation`
- Major Deliverables: integrity audit scripts, placeholder registry, parity hash checks, audit artifact generation, stricter integrity gating.
- Key Files: `docs/implementation/task-h51-pdf-integrity-audit.md`, `docs/business/pdf-placeholder-registry.md`, `scripts/audit-approved-snapshot-parity.ts`
- Read Before Modify: `task-h51-pdf-integrity-audit.md`, `task-h5-pdf-mapping-audit-visual-parity.md`, `task-h53-pdf-template-version-remap.md`
### Task H.5.2
- Task ID: `Task H.5.2`
- Task Name: `Historical Record Missing`
- Status: `Missing historical record`
- Objective: No dedicated completed implementation document was found for `Task H.5.2` in `docs/implementation/**`.
- Related ADRs: `Not confirmed`
- Foundations Created: `Unknown`
- Foundations Modified: `Unknown`
- Major Deliverables: no task note found; review adjacent H.5, H.5.1, H.5.3, `pdf-audit-report.md`, and `pdf-audit-report.json` for continuity.
- Key Files: `docs/implementation/task-h5-pdf-mapping-audit-visual-parity.md`, `docs/implementation/task-h51-pdf-integrity-audit.md`, `docs/implementation/task-h53-pdf-template-version-remap.md`, `docs/implementation/pdf-audit-report.md`
- Read Before Modify: `task-h5-pdf-mapping-audit-visual-parity.md`, `task-h51-pdf-integrity-audit.md`, `task-h53-pdf-template-version-remap.md`
### Task H.5.3
- Task ID: `Task H.5.3`
- Task Name: `PDF Template Version Remap + Static Labels`
- Status: `Completed`
- Objective: Move active templates to remapped versioned records and restore runtime-managed static labels.
- Related ADRs: `ADR-0013`
- Foundations Created: `Template remap workflow within Document Template Foundation`
- Foundations Modified: `Document Template Foundation`, `PDF Foundation`
- Major Deliverables: active `1.1` template versions, remap workflow, retained inactive historical versions, restored static labels, passing audit suite.
- Key Files: `docs/implementation/task-h53-pdf-template-version-remap.md`, `src/pdfme_template/*.json`, `docs/business/pdf-mapping-registry.md`
- Read Before Modify: `task-h53-pdf-template-version-remap.md`, `task-h51-pdf-integrity-audit.md`, `pdf-mapping-registry.md`, `pdf-placeholder-registry.md`
### Task I
- Task ID: `Task I`
- Task Name: `Storage Abstraction and Approved Artifact Lifecycle`
- Status: `Completed`
- Objective: Replace raw public-file assumptions with a storage provider and artifact lifecycle foundation.
- Related ADRs: `ADR-0008`, `ADR-0009`
- Foundations Created: `Storage Foundation`, `Document Artifact Foundation`
- Foundations Modified: `PDF Foundation`, `Approval Foundation`
- Major Deliverables: storage provider abstraction, artifact schema, secure download route, checksum support, approved-artifact locking, compatibility migration path.
- Key Files: `docs/implementation/task-i-storage-artifact-lifecycle.md`, `src/features/foundation/storage/**`, `src/features/foundation/document-artifact/**`
- Read Before Modify: `task-i-storage-artifact-lifecycle.md`, `0009-approved-document-storage-lifecycle.md`, `task-l3-full-crm-scope-enforcement.md`
### Task J.0
- Task ID: `Task J.0`
- Task Name: `KPI Definition Freeze`
- Status: `Completed`
- Objective: Freeze KPI semantics, party terminology, and revenue rules before dashboard implementation.
- Related ADRs: `Pre-ADR governance freeze; later intersected by ADR-0010, ADR-0011, ADR-0016`
- Foundations Created: `None`
- Foundations Modified: `Dashboard Foundation` lineage, `Revenue Attribution Foundation` lineage
- Major Deliverables: KPI definitions, pipeline semantics, party terminology freeze, governance rule that later KPI changes require an ADR.
- Key Files: `docs/implementation/task-j0-kpi-definition-freeze.md`
- Read Before Modify: `task-j0-kpi-definition-freeze.md`, `task-j-crm-dashboard-kpi.md`, `0010-revenue-attribution-governance.md`, `0011-lead-enquiry-ownership-model.md`
### Task J
- Task ID: `Task J`
- Task Name: `CRM Dashboard KPI and Sales Analytics`
- Status: `Completed`
- Objective: Deliver the live CRM dashboard and operational KPI surfaces on top of frozen governance rules.
- Related ADRs: `ADR-0010`, `ADR-0011`, `ADR-0016`
- Foundations Created: `Dashboard Foundation`
- Foundations Modified: `Revenue Attribution Foundation`, `Follow-Up Foundation`, `Approval Foundation`
- Major Deliverables: dashboard APIs, summary/funnel/revenue/sales/follow-up/approval widgets, export support, dashboard permissions.
- Key Files: `docs/implementation/task-j-crm-dashboard-kpi.md`, `src/features/crm/dashboard/**`, `src/app/api/crm/dashboard/**`
- Read Before Modify: `task-j-crm-dashboard-kpi.md`, `task-j0-kpi-definition-freeze.md`, `task-d21-revenue-attribution-governance.md`, `task-d4-won-lost-lifecycle-governance.md`
### Task J.1
- Task ID: `Task J.1`
- Task Name: `CRM Admin Configuration Center`
- Status: `Completed`
- Objective: Expose settings foundations through organization-scoped admin routes and usable production UI.
- Related ADRs: `ADR-0006`, `ADR-0012`
- Foundations Created: `None`
- Foundations Modified: `Approval Foundation`, `Document Sequence Foundation`, `Document Template Foundation`, `Master Data Foundation`
- Major Deliverables: approval-workflow settings UI and APIs, document-sequence settings UI and APIs, template edit/version/mapping UI and APIs, settings navigation.
- Key Files: `docs/implementation/task-j1-crm-admin-configuration-center.md`, `src/app/api/crm/settings/**`, `src/app/dashboard/crm/settings/**`
- Read Before Modify: `task-j1-crm-admin-configuration-center.md`, `project-foundations.md`, `task-g-document-preview-foundation.md`, `task-f-approval-production.md`
### Task K.1
- Task ID: `Task K.1`
- Task Name: `Report Foundation`
- Status: `Completed`
- Objective: Establish the shared CRM report platform before business report execution pages are added.
- Related ADRs: `ADR-0017`
- Foundations Created: `Reporting Foundation`
- Foundations Modified: `Master Data Foundation`, `Authorization Foundation`, `Audit Foundation`
- Major Deliverables: report registry, report category seed, report discovery APIs, dataset/builder/filter/export layers, report landing page.
- Key Files: `docs/implementation/task-k1-report-foundation.md`, `docs/adr/0017-report-foundation.md`, `src/features/crm/reports/**`
- Read Before Modify: `0017-report-foundation.md`, `task-k1-report-foundation.md`, `task-k2-pipeline-reports.md`, `project-foundations.md`
### Task K.2
- Task ID: `Task K.2`
- Task Name: `Pipeline Reports`
- Status: `Completed`
- Objective: Deliver the first production report suite on top of the shared report foundation.
- Related ADRs: `ADR-0010`, `ADR-0011`, `ADR-0016`, `ADR-0017`
- Foundations Created: `None`
- Foundations Modified: `Reporting Foundation`, `Follow-Up Foundation`, `Authorization Foundation`
- Major Deliverables: pipeline, lead-aging, and enquiry-aging reports; export route; dataset layer; scoped security enforcement; report navigation.
- Key Files: `docs/implementation/task-k2-pipeline-reports.md`, `src/app/api/crm/reports/**`, `src/features/crm/reports/server/datasets/pipeline-report.dataset.ts`
- Read Before Modify: `task-k2-pipeline-reports.md`, `task-k1-report-foundation.md`, `0017-report-foundation.md`, `task-d3-lead-enquiry-ownership-refinement.md`, `task-d4-won-lost-lifecycle-governance.md`
### Task L
- Task ID: `Task L`
- Task Name: `CRM Permission & Role Management`
- Status: `Completed`
- Objective: Replace template-era authorization behavior with app-owned CRM role profiles, permissions, and scope-aware access resolution.
- Related ADRs: `ADR-0014`
- Foundations Created: `Role Management Foundation`, `Authorization Foundation`
- Foundations Modified: `Dashboard Foundation`, `Permission Foundation`
- Major Deliverables: role profiles, permissions for pricing and admin settings, resolved CRM access helper, roles page and APIs, early server-side scope enforcement.
- Key Files: `docs/implementation/task-l-crm-permission-role-management.md`, `src/features/foundation/role-management/**`, `src/lib/auth/crm-access.ts`
- Read Before Modify: `task-l-crm-permission-role-management.md`, `0014-crm-multi-role-user-assignment.md`, `crm-authorization-boundaries.md`, `crm-access-enforcement-inventory.md`
### Task L.1
- Task ID: `Task L.1`
- Task Name: `CRM Multi-Role User Assignment`
- Status: `Completed`
- Objective: Move CRM authorization from one business role per membership to multi-role assignment rows.
- Related ADRs: `ADR-0014`
- Foundations Created: `CRM Role Assignment Foundation`
- Foundations Modified: `Authorization Foundation`, `Role Management Foundation`
- Major Deliverables: `crm_user_role_assignments`, assignment permissions, assignment APIs, compatibility backfill, union-based resolved access.
- Key Files: `docs/implementation/task-l1-crm-multi-role-user-assignment.md`, `docs/adr/0014-crm-multi-role-user-assignment.md`, `src/features/foundation/crm-role-assignments/**`
- Read Before Modify: `0014-crm-multi-role-user-assignment.md`, `task-l1-crm-multi-role-user-assignment.md`, `task-l2-user-management-integration.md`
### Task L.2
- Task ID: `Task L.2`
- Task Name: `User Management Integration`
- Status: `Completed`
- Objective: Make user management the primary operational surface for CRM role assignments.
- Related ADRs: `ADR-0014`
- Foundations Created: `None`
- Foundations Modified: `CRM Role Assignment Foundation`, `Authorization Foundation`, users feature integration
- Major Deliverables: user create/edit payload changes, CRM role assignment editing in user form, effective-access preview, reference endpoints, migration utility.
- Key Files: `docs/implementation/task-l2-user-management-integration.md`, `src/app/api/users/_lib.ts`, `src/app/api/users/[id]/effective-access-preview/route.ts`
- Read Before Modify: `task-l2-user-management-integration.md`, `task-l1-crm-multi-role-user-assignment.md`, `0014-crm-multi-role-user-assignment.md`
### Task L.3
- Task ID: `Task L.3`
- Task Name: `Full CRM Scope Enforcement`
- Status: `Completed`
- Objective: Harden scope and pricing visibility enforcement around resolved CRM access.
- Related ADRs: `ADR-0014`
- Foundations Created: `Security enforcement layer within Authorization Foundation`
- Foundations Modified: `Authorization Foundation`, `Document Artifact Foundation`, `Dashboard Foundation`, `Quotation Foundation`
- Major Deliverables: security helpers, pricing guards for quotation outputs, dashboard export restriction, quotation scope enforcement, security inventory docs, verification script.
- Key Files: `docs/implementation/task-l3-full-crm-scope-enforcement.md`, `src/features/crm/security/server/service.ts`, `docs/security/crm-authorization-boundaries.md`
- Read Before Modify: `task-l3-full-crm-scope-enforcement.md`, `crm-authorization-boundaries.md`, `crm-access-enforcement-inventory.md`, `team-scope-limitations.md`
### Task L.3.1
- Task ID: `Task L.3.1`
- Task Name: `Customer, Contact & Approval Visibility Hardening`
- Status: `Completed`
- Objective: Extend resolved-access enforcement to customer, contact, and approval visibility.
- Related ADRs: `ADR-0015`
- Foundations Created: `None`
- Foundations Modified: `Customer Ownership Foundation`, `Contact Sharing Foundation`, `Approval Foundation`, `Authorization Foundation`, `Dashboard Foundation`
- Major Deliverables: customer visibility filters, contact visibility inheritance, approval visibility narrowing, dashboard approval reuse, extra security audit actions.
- Key Files: `docs/implementation/task-l31-customer-contact-approval-visibility.md`, `src/features/crm/customers/server/service.ts`, `src/features/foundation/approval/server/service.ts`
- Read Before Modify: `task-l31-customer-contact-approval-visibility.md`, `0015-customer-ownership-contact-sharing.md`, `crm-authorization-boundaries.md`, `task-c1-customer-ownership-contact-sharing-governance.md`
### Task UX.1
- Task ID: `Task UX.1`
- Task Name: `Thai CRM Terminology Standardization`
- Status: `Completed`
- Objective: Freeze Thai business-facing CRM terminology while keeping internal codes stable.
- Related ADRs: `None dedicated`
- Foundations Created: `Terminology registry support for UI governance`
- Foundations Modified: `Dashboard Foundation`, `Lead / Enquiry Foundation`, `PDF Foundation`
- Major Deliverables: terminology registry, shared terminology helper, menu copy updates, dashboard/report/PDF copy updates.
- Key Files: `docs/implementation/task-ux1-thai-crm-terminology-standardization.md`, `docs/business/crm-terminology.md`, `src/features/crm/shared/terminology.ts`
- Read Before Modify: `task-ux1-thai-crm-terminology-standardization.md`, `crm-terminology.md`, `ui-ux-rules.md`
### Hotfix 1
- Task ID: `Hotfix 1`
- Task Name: `Customer Sub Group`
- Status: `Completed`
- Objective: Add customer subgroup hierarchy without creating a parallel category system.
- Related ADRs: `None dedicated`
- Foundations Created: `None`
- Foundations Modified: `Master Data Foundation`, `Customer module`
- Major Deliverables: `customerSubGroup` field, seeded `crm_customer_group` and `crm_customer_sub_group`, parent/child validation, filtered customer form behavior.
- Key Files: `docs/implementation/hotfix-1-customer-sub-group.md`, `src/db/seeds/foundation.seed.ts`, `src/features/crm/customers/server/service.ts`
- Read Before Modify: `hotfix-1-customer-sub-group.md`, `project-foundations.md`, `task-c-customer-contact.md`
## Supporting Historical Records
These are not primary task IDs, but they remain valuable historical review documents:
- `docs/implementation/pdfme-legacy-audit.md`
- `docs/implementation/pdfme-runtime-audit.md`
- `docs/implementation/pdf-parity-checklist.md`
- `docs/implementation/pdf-audit-report.md`
- `docs/implementation/pdf-audit-report.json`
- `docs/implementation/technical-debt.md`
## Missing Historical Records
- `Task H.5.2` has no dedicated implementation note in `docs/implementation/**`.
- No standalone ADR was found for the low-level early foundations created in Task B:
- `Auth Context Foundation`
- `Organization Context Foundation`
- `Permission Foundation`
- `Audit Foundation`
- `Document Sequence Foundation`
## Catalog Outcome
- ALLA OS Historical Knowledge Base = ESTABLISHED
- ALLA OS Task Discovery = READY
- ALLA OS AI Context Retention = IMPROVED

View File

@@ -0,0 +1,148 @@
# Task Contract Template
Use this template for every future task before implementation starts.
The contract exists to force review-first execution, foundation reuse, and explicit scope control.
---
## Task Information
- Task ID:
- Title:
- Owner:
- Requested by:
- Date:
- Related ticket / plan:
## Objective
- Describe the business outcome, not just the code change.
## Business Context
- Relevant domain background:
- Why this task exists now:
- Related ADRs:
- Related business docs:
## Required Skills & Context
- `AGENTS.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`
- Relevant `docs/adr/**`
- Relevant `docs/business/**`
- Relevant `docs/security/**`
- Existing feature and API references:
- Historical task documents reviewed:
## Architecture Constraints
- Frontend constraints:
- Backend constraints:
- Security constraints:
- Reuse constraints:
- Forbidden patterns:
## Reuse Existing Foundations
- Foundations reviewed:
- Existing services to extend:
- Existing APIs to extend:
- Existing query/mutation patterns to reuse:
- Existing UI shells/components to reuse:
- Existing audit patterns to reuse:
- Why a new foundation is not required:
## Data Model Changes
- Schema changes required:
- Existing tables impacted:
- Migration required:
- Explicit non-changes:
## API Requirements
- Route handlers involved:
- Request/response contracts:
- Service layer functions:
- Validation rules:
- Existing APIs reused or extended:
## UI Requirements
- Routes / screens:
- Page shell pattern:
- Form / table / filter patterns:
- Terminology requirements:
- Empty / loading / error states:
## Report / Export Rules
- Report foundation reviewed:
- Shared filters reused:
- Dataset layer reused:
- Export path reused:
- Revenue/pricing visibility rule:
## Permission Requirements
- Required permissions:
- Scope enforcement requirements:
- Resolved access helper to use:
- Server-side protection points:
## Audit Requirements
- Entity type:
- Actions:
- Required payload:
- Existing audit naming reused:
## Scope
- In scope:
## Explicit Non-Scope
- Out of scope:
## Security Verification Matrix
| Scenario | Expected access | Enforcement point |
| --- | --- | --- |
| Admin | | |
| Scoped user | | |
| Unauthorized user | | |
| Pricing-restricted user | | |
## Deliverables
- Documentation deliverables:
- Code deliverables:
- Verification deliverables:
## Verification
- Typecheck:
- Lint:
- Targeted tests:
- Manual scenarios:
## Documentation
- Docs to update:
- ADR needed:
- Task implementation note:
## Definition of Done
- Review completed before implementation
- Existing foundations reused or extension rationale documented
- Security and audit requirements verified
- Documentation updated
- Verification evidence recorded

View File

@@ -0,0 +1,82 @@
# Task Review Checklist
Complete this checklist before implementation starts.
## Foundation Review
- [ ] Reviewed `AGENTS.md`
- [ ] Reviewed `docs/standards/project-foundations.md`
- [ ] Reviewed the relevant reusable foundations under `src/features/foundation/**`
- [ ] Reviewed the relevant CRM feature foundations under `src/features/crm/**`
- [ ] Confirmed whether the task extends an existing service before creating a new one
## ADR Review
- [ ] Reviewed relevant `docs/adr/**`
- [ ] Confirmed whether the requested behavior conflicts with an accepted ADR
- [ ] Documented if a new ADR is required
## Historical Task Review
- [ ] Identified related foundations in `docs/standards/project-foundations.md`
- [ ] Identified related ADRs
- [ ] Identified the tasks that created or changed those foundations in `docs/standards/task-catalog.md`
- [ ] Reviewed the related completed task documents in `docs/implementation/**`
- [ ] Documented historical findings before implementation
Implementation without historical review is prohibited.
## Business Context Review
- [ ] Reviewed relevant `docs/business/**`
- [ ] Confirmed business terminology and scope
- [ ] Checked whether Thai CRM terminology rules apply
## API Review
- [ ] Reviewed existing route handlers under the affected `src/app/api/**` area
- [ ] Confirmed whether an existing API can be extended instead of duplicated
- [ ] Confirmed request/response contracts and validation approach
## Permission Review
- [ ] Reviewed `src/lib/auth/session.ts`
- [ ] Reviewed `src/lib/auth/crm-access.ts` if CRM scope is involved
- [ ] Reviewed `src/features/crm/security/server/service.ts` if CRM security is involved
- [ ] Reviewed relevant `docs/security/**`
- [ ] Confirmed server-side enforcement points
## Audit Review
- [ ] Reviewed `src/features/foundation/audit-log/service.ts`
- [ ] Checked existing entity/action names for the affected module
- [ ] Confirmed whether security-denial auditing is required
- [ ] Confirmed audit payload expectations
## Duplication Review
- [ ] No duplicate services
- [ ] No duplicate APIs
- [ ] No duplicate permissions or role models
- [ ] No duplicate export/report systems
- [ ] No duplicate PDF rendering path
- [ ] No duplicate approval workflow logic
## Security Review
- [ ] Direct role-string authorization is not used
- [ ] Scope enforcement is server-side
- [ ] Branch/product/ownership/pricing visibility is enforced where applicable
- [ ] Client-side visibility is treated as UX-only, not as security
## UI / UX Review
- [ ] Reviewed `docs/standards/ui-ux-rules.md`
- [ ] Reused `PageContainer`, table, filter, and form patterns where applicable
- [ ] Confirmed terminology consistency with `docs/business/crm-terminology.md`
## Verification Review
- [ ] Defined typecheck/lint/test verification
- [ ] Defined manual scenarios for scope/security-sensitive work
- [ ] Recorded known risks and follow-up items

View File

@@ -0,0 +1,111 @@
# UI / UX Rules
These rules freeze the approved UI behavior for ALLA OS and CRM-facing work.
Future UI should reuse existing dashboard patterns rather than introducing a parallel visual system.
## UX.1 Thai CRM Terminology
- `docs/business/crm-terminology.md` is the source of truth for CRM business labels.
- Use `src/features/crm/shared/terminology.ts` when CRM UI needs shared stage or role labels.
- Keep `CRM` and `Dashboard` in English where the existing product already does so.
- Do not rename schema fields, API routes, or internal codes just to match UI wording.
## Dashboard Layout Standards
- Use `PageContainer` for dashboard page framing and page headers.
- Keep pages inside the existing dashboard shell; do not build a separate CRM shell.
- Prefer small, composable sections instead of one oversized page component.
- Use cards, tables, and filter bars consistent with existing CRM dashboard and report pages.
Reference files:
- `src/components/layout/page-container.tsx`
- `src/features/crm/dashboard/components/crm-dashboard.tsx`
- `src/features/crm/reports/components/pipeline-report-view.tsx`
## Bento Grid Guidelines
- Reuse the existing dashboard-card rhythm: summary cards first, then charts/tables/detail sections.
- Keep KPI modules visually scannable with clear grouping and limited density per row.
- Avoid creating a radically different grid language inside CRM pages.
- Match existing spacing, border radius, and card hierarchy from current dashboard/report pages.
## Form Standards
- Use TanStack Form via `@/components/ui/tanstack-form`.
- Use Zod for submit validation.
- Reuse field components from `@/components/forms/fields`.
- Keep labels and helper text business-readable.
- Use `<Button isLoading={isPending}>` for submit/loading actions.
- Close sheets/dialogs only after successful mutation completion and cache invalidation.
Reference docs and files:
- `docs/forms.md`
- `src/features/crm/customers/components/customer-form-sheet.tsx`
- `src/features/crm/opportunities/components/opportunity-form-sheet.tsx`
- `src/features/crm/quotations/components/quotation-form-sheet.tsx`
## Table Standards
- Use `DataTable`, `DataTableToolbar`, and `useDataTable`.
- Define columns close to the feature component tree.
- Keep filters URL-driven when the list page is shareable/bookmarkable.
- Use `useSuspenseQuery()` for table data that is prefetched on the server.
Reference files:
- `src/components/ui/table/data-table.tsx`
- `src/features/users/components/users-table/index.tsx`
- `src/features/crm/customers/components/customers-table.tsx`
- `src/features/crm/opportunities/components/opportunities-table.tsx`
## Filter Standards
- Reuse shared filter metadata and existing select/date input patterns.
- Prefer `nuqs` query-state filters for reports and list pages.
- Keep "clear filters" behavior explicit.
- Report exports must use the same filter contract as on-screen data.
Reference files:
- `src/features/crm/reports/components/report-filter-bar.tsx`
- `src/features/crm/reports/server/filters/shared.ts`
## Empty State Standards
- Empty states should explain what is missing and what the next action is.
- Prefer existing placeholder/skeleton components before inventing one-off empty-state systems.
- Keep empty states within the same page shell and card layout as loaded content.
## Loading State Standards
- Use skeletons or suspense-friendly loading states for lists, charts, and dashboard panels.
- Preserve layout stability while loading.
- Keep button loading width stable with the existing button loading pattern.
Reference files:
- `src/components/ui/table/data-table-skeleton.tsx`
- `src/features/overview/components/*-skeleton.tsx`
- `src/components/ui/button.tsx`
## Error State Standards
- Use existing route-level `error.tsx` boundaries where available.
- Return actionable copy for recoverable failures.
- Do not expose raw backend or SQL detail in business-facing UI.
- Security denials should remain server-enforced even if the UI also hides the surface.
Reference files:
- `src/app/dashboard/overview/error.tsx`
- `src/app/global-error.tsx`
## Reuse Rules
- Do not create a new CRM design language.
- Do not create a second terminology registry.
- Do not create report-specific filter widgets when shared report filters already fit.
- Do not introduce UI-level permission checks as the only enforcement layer.

View File

@@ -1,6 +1,6 @@
import { defineConfig } from "drizzle-kit";
import "dotenv/config";
console.log("DATABASE_URL is:", process.env.DATABASE_URL); // ดูผลลัพธ์ตอนรัน drizzle-kit
console.log("DATABASE_URL is:", process.env.DATABASE_URL); // Inspect env while running drizzle-kit
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",

View File

@@ -0,0 +1,657 @@
CREATE TABLE "crm_approval_actions" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"approval_request_id" text NOT NULL,
"step_number" integer NOT NULL,
"action" text NOT NULL,
"remark" text,
"acted_by" text NOT NULL,
"acted_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_approval_requests" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"workflow_id" text NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"current_step" integer DEFAULT 1 NOT NULL,
"status" text NOT NULL,
"requested_by" text NOT NULL,
"requested_at" timestamp with time zone DEFAULT now() NOT NULL,
"completed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_approval_steps" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"workflow_id" text NOT NULL,
"step_number" integer NOT NULL,
"role_code" text NOT NULL,
"role_name" text NOT NULL,
"is_required" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_approval_workflows" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"code" text NOT NULL,
"name" text NOT NULL,
"entity_type" text NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_contact_shares" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"contact_id" text NOT NULL,
"shared_to_user_id" text NOT NULL,
"shared_by_user_id" text NOT NULL,
"shared_at" timestamp with time zone DEFAULT now() NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"remark" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_customer_contacts" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"customer_id" text NOT NULL,
"name" text NOT NULL,
"position" text,
"department" text,
"phone" text,
"mobile" text,
"email" text,
"is_primary" boolean DEFAULT false NOT NULL,
"notes" text,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_customer_owner_history" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"customer_id" text NOT NULL,
"old_owner_user_id" text,
"new_owner_user_id" text,
"changed_by" text NOT NULL,
"changed_at" timestamp with time zone DEFAULT now() NOT NULL,
"remark" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_customers" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"code" text NOT NULL,
"name" text NOT NULL,
"abbr" text,
"tax_id" text,
"customer_type" text NOT NULL,
"customer_status" text NOT NULL,
"address" text,
"province" text,
"district" text,
"sub_district" text,
"postal_code" text,
"country" text,
"phone" text,
"fax" text,
"email" text,
"website" text,
"lead_channel" text,
"customer_group" text,
"customer_sub_group" text,
"owner_user_id" text,
"owner_assigned_at" timestamp with time zone,
"owner_assigned_by" text,
"notes" text,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_document_artifacts" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"document_type" text NOT NULL,
"artifact_type" text NOT NULL,
"template_version_id" text,
"storage_provider" text NOT NULL,
"storage_key" text NOT NULL,
"file_name" text NOT NULL,
"content_type" text NOT NULL,
"file_size" integer,
"checksum" text,
"status" text DEFAULT 'active' NOT NULL,
"generated_by" text NOT NULL,
"generated_at" timestamp with time zone DEFAULT now() NOT NULL,
"locked_at" timestamp with time zone,
"locked_by" text,
"voided_at" timestamp with time zone,
"voided_by" text,
"void_reason" text,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_document_template_mappings" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"template_version_id" text NOT NULL,
"placeholder_key" text NOT NULL,
"source_path" text NOT NULL,
"data_type" text NOT NULL,
"sheet_name" text,
"default_value" text,
"format_mask" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_document_template_table_columns" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"mapping_id" text NOT NULL,
"column_name" text NOT NULL,
"source_field" text NOT NULL,
"column_letter" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"format_mask" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_document_template_versions" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"template_id" text NOT NULL,
"version" text NOT NULL,
"file_path" text,
"schema_json" jsonb NOT NULL,
"preview_image_url" text,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_document_templates" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"document_type" text NOT NULL,
"product_type" text NOT NULL,
"file_type" text NOT NULL,
"template_name" text NOT NULL,
"description" text,
"is_default" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_leads" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"code" text NOT NULL,
"customer_id" text,
"contact_id" text,
"description" text,
"project_name" text,
"project_location" text,
"product_type" text,
"priority" text,
"estimated_value" double precision,
"awareness_id" text,
"status" text DEFAULT 'new_job' NOT NULL,
"followup_status" text,
"lost_reason" text,
"outcome" text DEFAULT 'open' NOT NULL,
"owner_marketing_user_id" text,
"assigned_sales_owner_id" text,
"assigned_at" timestamp with time zone,
"assigned_by" text,
"assignment_remark" text,
"created_by" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_opportunities" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"lead_id" text,
"code" text NOT NULL,
"customer_id" text NOT NULL,
"contact_id" text,
"title" text NOT NULL,
"description" text,
"requirement" text,
"project_name" text,
"project_location" text,
"product_type" text NOT NULL,
"status" text NOT NULL,
"priority" text NOT NULL,
"lead_channel" text,
"estimated_value" double precision,
"chance_percent" integer,
"expected_close_date" timestamp with time zone,
"competitor" text,
"source" text,
"notes" text,
"is_hot_project" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"pipeline_stage" text DEFAULT 'lead' NOT NULL,
"closed_won_at" timestamp with time zone,
"closed_lost_at" timestamp with time zone,
"closed_by_user_id" text,
"po_number" text,
"po_date" timestamp with time zone,
"po_amount" double precision,
"po_currency" text,
"lost_reason" text,
"lost_competitor" text,
"lost_remark" text,
"assigned_to_user_id" text,
"assigned_at" timestamp with time zone,
"assigned_by" text,
"assignment_remark" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_opportunity_attachments" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"opportunity_id" text NOT NULL,
"category" text NOT NULL,
"file_name" text NOT NULL,
"original_file_name" text NOT NULL,
"storage_provider" text NOT NULL,
"storage_key" text NOT NULL,
"file_size" integer,
"file_type" text,
"description" text,
"uploaded_at" timestamp with time zone DEFAULT now() NOT NULL,
"uploaded_by" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_opportunity_customers" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"opportunity_id" text NOT NULL,
"customer_id" text NOT NULL,
"role" text NOT NULL,
"remark" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_opportunity_followups" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"opportunity_id" text NOT NULL,
"followup_date" timestamp with time zone NOT NULL,
"followup_type" text NOT NULL,
"contact_id" text,
"outcome" text,
"notes" text,
"next_followup_date" timestamp with time zone,
"next_action" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_attachments" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"quotation_id" text NOT NULL,
"file_name" text NOT NULL,
"original_file_name" text NOT NULL,
"file_path" text NOT NULL,
"file_size" integer,
"file_type" text,
"description" text,
"uploaded_at" timestamp with time zone DEFAULT now() NOT NULL,
"uploaded_by" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_customers" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"quotation_id" text NOT NULL,
"customer_id" text NOT NULL,
"role" text NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"remark" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_followups" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"quotation_id" text NOT NULL,
"followup_date" timestamp with time zone NOT NULL,
"followup_type" text NOT NULL,
"contact_id" text,
"outcome" text,
"notes" text,
"next_followup_date" timestamp with time zone,
"next_action" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_items" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"quotation_id" text NOT NULL,
"item_number" integer NOT NULL,
"product_type" text NOT NULL,
"description" text NOT NULL,
"quantity" double precision DEFAULT 0 NOT NULL,
"unit" text,
"unit_price" double precision DEFAULT 0 NOT NULL,
"discount" double precision DEFAULT 0 NOT NULL,
"discount_type" text,
"tax_rate" double precision DEFAULT 0 NOT NULL,
"total_price" double precision DEFAULT 0 NOT NULL,
"notes" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_topic_items" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"topic_id" text NOT NULL,
"content" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_quotation_topics" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"quotation_id" text NOT NULL,
"topic_type" text NOT NULL,
"title" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "crm_quotations" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"code" text NOT NULL,
"opportunity_id" text,
"customer_id" text NOT NULL,
"contact_id" text,
"quotation_date" timestamp with time zone NOT NULL,
"valid_until" timestamp with time zone,
"quotation_type" text NOT NULL,
"project_name" text,
"project_location" text,
"attention" text,
"reference" text,
"notes" text,
"status" text NOT NULL,
"revision" integer DEFAULT 0 NOT NULL,
"parent_quotation_id" text,
"revision_remark" text,
"currency" text NOT NULL,
"exchange_rate" double precision DEFAULT 1 NOT NULL,
"subtotal" double precision DEFAULT 0 NOT NULL,
"discount" double precision DEFAULT 0 NOT NULL,
"discount_type" text,
"tax_rate" double precision DEFAULT 0 NOT NULL,
"tax_amount" double precision DEFAULT 0 NOT NULL,
"total_amount" double precision DEFAULT 0 NOT NULL,
"chance_percent" integer,
"is_hot_project" boolean DEFAULT false NOT NULL,
"competitor" text,
"salesman_id" text,
"is_sent" boolean DEFAULT false NOT NULL,
"sent_at" timestamp with time zone,
"sent_via" text,
"approved_at" timestamp with time zone,
"approved_artifact_id" text,
"approved_pdf_url" text,
"approved_snapshot" jsonb,
"approved_template_version_id" text,
"accepted_at" timestamp with time zone,
"rejected_at" timestamp with time zone,
"rejection_reason" text,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_report_definitions" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"code" text NOT NULL,
"name" text NOT NULL,
"description" text,
"category" text NOT NULL,
"is_system" boolean DEFAULT true NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_role_profiles" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"code" text NOT NULL,
"name" text NOT NULL,
"description" text,
"permissions" text[] DEFAULT '{}' NOT NULL,
"branch_scope_mode" text DEFAULT 'assigned' NOT NULL,
"product_scope_mode" text DEFAULT 'assigned' NOT NULL,
"ownership_scope" text DEFAULT 'own' NOT NULL,
"approval_authority" text DEFAULT 'none' NOT NULL,
"is_system" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"created_by" text NOT NULL,
"updated_by" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crm_user_role_assignments" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role_profile_id" text NOT NULL,
"branch_scope_mode" text DEFAULT 'inherit' NOT NULL,
"branch_scope_ids" text[] DEFAULT '{}' NOT NULL,
"product_type_scope_mode" text DEFAULT 'inherit' NOT NULL,
"product_type_scope_ids" text[] DEFAULT '{}' NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"assigned_by" text NOT NULL,
"assigned_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "document_sequences" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text DEFAULT '' NOT NULL,
"document_type" text NOT NULL,
"prefix" text NOT NULL,
"period" text NOT NULL,
"current_number" integer DEFAULT 0 NOT NULL,
"padding_length" integer DEFAULT 3 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "memberships" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"organization_id" text NOT NULL,
"role" text DEFAULT 'user' NOT NULL,
"business_role" text DEFAULT 'sales_support' NOT NULL,
"permissions" text[] DEFAULT '{}' NOT NULL,
"branch_scope_ids" text[] DEFAULT '{}' NOT NULL,
"product_type_scope_ids" text[] DEFAULT '{}' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ms_options" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"category" text NOT NULL,
"code" text NOT NULL,
"label" text NOT NULL,
"value" text,
"parent_id" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"metadata" jsonb,
"deleted_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organizations" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"image_url" text,
"plan" text DEFAULT 'free' NOT NULL,
"created_by" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "products" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "products_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"organization_id" text NOT NULL,
"name" text NOT NULL,
"category" text NOT NULL,
"description" text NOT NULL,
"photo_url" text NOT NULL,
"price" double precision NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tr_audit_logs" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"user_id" text NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"action" text NOT NULL,
"before_data" jsonb,
"after_data" jsonb,
"request_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"password_hash" text NOT NULL,
"system_role" text DEFAULT 'user' NOT NULL,
"image" text,
"active_organization_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "crm_approval_steps_workflow_step_idx" ON "crm_approval_steps" USING btree ("workflow_id","step_number");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_approval_workflows_org_code_idx" ON "crm_approval_workflows" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_contact_shares_org_contact_shared_user_idx" ON "crm_contact_shares" USING btree ("organization_id","contact_id","shared_to_user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_customers_org_code_idx" ON "crm_customers" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_document_template_mappings_version_placeholder_idx" ON "crm_document_template_mappings" USING btree ("template_version_id","placeholder_key");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_document_template_table_columns_mapping_name_idx" ON "crm_document_template_table_columns" USING btree ("mapping_id","column_name");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_document_template_versions_template_version_idx" ON "crm_document_template_versions" USING btree ("template_id","version");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_document_templates_org_doc_product_file_name_idx" ON "crm_document_templates" USING btree ("organization_id","document_type","product_type","file_type","template_name");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_leads_org_code_idx" ON "crm_leads" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_opportunities_org_code_idx" ON "crm_opportunities" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_quotations_org_code_idx" ON "crm_quotations" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_report_definitions_org_code_idx" ON "crm_report_definitions" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_role_profiles_org_code_idx" ON "crm_role_profiles" USING btree ("organization_id","code");--> statement-breakpoint
CREATE UNIQUE INDEX "crm_user_role_assignments_org_user_role_idx" ON "crm_user_role_assignments" USING btree ("organization_id","user_id","role_profile_id");--> statement-breakpoint
CREATE UNIQUE INDEX "document_sequences_org_doc_period_branch_idx" ON "document_sequences" USING btree ("organization_id","document_type","period","branch_id");--> statement-breakpoint
CREATE UNIQUE INDEX "ms_options_org_category_code_idx" ON "ms_options" USING btree ("organization_id","category","code");--> statement-breakpoint
CREATE UNIQUE INDEX "organizations_slug_idx" ON "organizations" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");

View File

@@ -0,0 +1,5 @@
ALTER TABLE "crm_opportunities" ADD COLUMN "outcome_status" text DEFAULT 'open' NOT NULL;--> statement-breakpoint
ALTER TABLE "crm_opportunities" ADD COLUMN "closed_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "crm_opportunities" ADD COLUMN "lost_detail" text;--> statement-breakpoint
ALTER TABLE "crm_opportunities" ADD COLUMN "cancel_reason" text;--> statement-breakpoint
ALTER TABLE "crm_opportunities" ADD COLUMN "no_quotation_reason" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,15 @@
{
"idx": 0,
"version": "7",
"when": 1781148450992,
"tag": "0000_icy_lizard",
"when": 1782359607850,
"tag": "0000_hard_butterfly",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1782381033930,
"tag": "0001_lazy_otto_octavius",
"breakpoints": true
}
]

View File

@@ -3,7 +3,7 @@ CREATE TABLE "memberships" (
"user_id" text NOT NULL,
"organization_id" text NOT NULL,
"role" text DEFAULT 'user' NOT NULL,
"business_role" text DEFAULT 'viewer' NOT NULL,
"business_role" text DEFAULT 'sales_support' NOT NULL,
"permissions" text[] DEFAULT '{}' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
@@ -45,4 +45,4 @@ CREATE TABLE "users" (
);
--> statement-breakpoint
CREATE UNIQUE INDEX "organizations_slug_idx" ON "organizations" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");

View File

@@ -0,0 +1,46 @@
CREATE TABLE "document_sequences" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text DEFAULT '' NOT NULL,
"document_type" text NOT NULL,
"prefix" text NOT NULL,
"period" text NOT NULL,
"current_number" integer DEFAULT 0 NOT NULL,
"padding_length" integer DEFAULT 3 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ms_options" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"category" text NOT NULL,
"code" text NOT NULL,
"label" text NOT NULL,
"value" text,
"parent_id" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"metadata" jsonb,
"deleted_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tr_audit_logs" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"branch_id" text,
"user_id" text NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"action" text NOT NULL,
"before_data" jsonb,
"after_data" jsonb,
"request_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "document_sequences_org_doc_period_branch_idx" ON "document_sequences" USING btree ("organization_id","document_type","period","branch_id");--> statement-breakpoint
CREATE UNIQUE INDEX "ms_options_org_category_code_idx" ON "ms_options" USING btree ("organization_id","category","code");

Some files were not shown because too many files have changed in this diff Show More