This commit is contained in:
phaichayon
2026-06-23 22:13:08 +07:00
parent 99a4087099
commit c1ecd5ea50
32 changed files with 3503 additions and 150 deletions

135
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 ## Technology Stack Details
### Core Runtime ### Core Runtime
@@ -516,3 +637,17 @@ Ensure these are set:
17. When deleting entities, remove stale detail queries with `removeQueries` in addition to invalidating lists. 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. 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. 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

@@ -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,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,364 @@
# 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, enquiry, 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 / Enquiry Foundation
| Field | Details |
| --- | --- |
| Purpose | Single-record `crm_enquiries` lifecycle with marketing-owned leads, sales-owned enquiries, assignment governance, related project parties, and follow-up child resources |
| Location | `src/features/crm/enquiries/**`, `src/app/api/crm/enquiries/**`, `docs/adr/0011-lead-enquiry-ownership-model.md` |
| Related tasks | Task D, Task D.1, Task D.3, Task D.3.1 |
| Related ADRs | ADR-0011, ADR-0014 |
| Reusable services | `getEnquiryReferenceData()`, `listEnquiries()`, `getEnquiryDetail()`, `createEnquiry()`, `updateEnquiry()`, `softDeleteEnquiry()`, `assignEnquiryToUser()`, `reassignEnquiryToUser()`, `listEnquiryProjectParties()` |
| Reusable APIs | `/api/crm/enquiries`, `/api/crm/enquiries/[id]`, `/api/crm/enquiries/[id]/assign`, `/api/crm/enquiries/[id]/reassign`, `/api/crm/enquiries/[id]/customers` |
| Reuse status | Reusable and active. This is the source of truth for lead versus enquiry separation. |
### Won / Lost Governance Foundation
| Field | Details |
| --- | --- |
| Purpose | Freeze business outcome lifecycle on `crm_enquiries`, including PO capture, lost reason governance, and reopen rules |
| Location | `src/features/crm/enquiries/**`, `src/app/api/crm/enquiries/[id]/mark-won/route.ts`, `src/app/api/crm/enquiries/[id]/mark-lost/route.ts`, `src/app/api/crm/enquiries/[id]/reopen/route.ts` |
| Related tasks | Task D.4, Task J, Task K |
| Related ADRs | ADR-0016 |
| Reusable services | `markEnquiryAsWon()`, `markEnquiryAsLost()`, `reopenLostEnquiry()`, PO attachment helpers under enquiry service |
| Reusable APIs | `/api/crm/enquiries/[id]/mark-won`, `/api/crm/enquiries/[id]/mark-lost`, `/api/crm/enquiries/[id]/reopen`, `/api/crm/enquiries/[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 enquiries and quotations, including list/detail child resources, dashboard analytics, and report visibility |
| Location | `src/features/crm/enquiries/**`, `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 | `listEnquiryFollowups()`, `createEnquiryFollowup()`, `updateEnquiryFollowup()`, `softDeleteEnquiryFollowup()`, 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/enquiries/[id]/followups`, `/api/crm/enquiries/[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 enquiry 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()`, `getCrmEnquiryAgingReportResponse()`, `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/enquiry-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
- 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,622 @@
# 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 |
| 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 |
## 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 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 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/enquiries/components/enquiry-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/enquiries/components/enquiries-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.

766
plans/task-d.5.md Normal file
View File

@@ -0,0 +1,766 @@
# Task D.5: Lead / Enquiry Domain Separation
## Objective
Refactor CRM pipeline architecture by separating Lead and Enquiry into dedicated business entities.
Replace the current shared `crm_enquiries + pipelineStage` model with a true business flow:
```txt
Lead
→ Enquiry
→ Quotation
→ Won / Lost
```
The new model must support:
```txt
1 Lead
→ N Enquiries
1 Enquiry
→ N Quotations
1 Quotation
→ N Options
```
and remove the ambiguity caused by stage-based mutation.
---
# Business Discovery 3.1 (Frozen)
## Marketing Flow
```txt
Marketing
Create Lead
Follow Up
Assign Sales
Create Enquiry
```
Marketing owns Leads.
---
## Sales Flow
```txt
Sales
Create Enquiry Directly
```
Sales can create Enquiries without a Lead.
---
## Sales Execution Flow
```txt
Enquiry
Quotation
PO Received
Won
```
or
```txt
Enquiry
Lost
```
---
# Mandatory Review
Before implementation review:
* AGENTS.md
* Project Foundations Registry
* Task Catalog
* ADR 0011 Lead / Enquiry Ownership Model
* ADR 0016 Won / Lost Lifecycle Governance
* Customer Ownership Foundation
* Reporting Foundation
* Authorization Foundation
* Dashboard Foundation
---
# Required Skills
## Governance
* AGENTS.md
* Task Contract Template
* Architecture Rules
* Task Review Checklist
---
## CRM
* Lead / Enquiry Governance
* Won / Lost Governance
* Customer Ownership Governance
* Revenue Attribution Governance
---
## Security
* CRM Authorization Foundation
* Scope Enforcement Foundation
---
# Scope D5.1 Lead Table
Create:
```txt
crm_leads
```
Lead becomes a dedicated entity.
Core fields:
```txt
id
organizationId
branchId
code
customerId
contactId
projectName
projectLocation
awarenessId
status
followupStatus
lostReason
outcome
ownerMarketingUserId
createdBy
createdAt
updatedAt
deletedAt
```
---
# Scope D5.2 Lead Awareness
Master option category:
```txt
crm_lead_awareness
```
Seed:
```txt
Google Search / Website
Social Media
Email Marketing
Visit
Exhibition / Event
Brochure / Catalog
Word of Mouth
Corporate Car
Product Label
Re-purchasing
Introducing
BCI
```
Purpose:
```txt
ลูกค้ารู้จักเราจากช่องทางใด
```
Used in:
* Lead Form
* Lead Dashboard
* Lead Reports
* Marketing Analytics
---
# Scope D5.3 Lead Status
Master option:
```txt
crm_lead_status
```
Values:
```txt
new_job
follow_up
closed_lost
cancel
```
Display:
```txt
New Job
Follow Up
Closed Lost
Cancel
```
---
# Scope D5.4 Lead Follow-up Status
Master option:
```txt
crm_lead_followup_status
```
Values:
```txt
budget_pending
project_under_review
spec_clarification
site_survey_waiting
drawing_followup
quotation_preparation
waiting_po
```
Display:
```txt
รองบประมาณ
ระหว่างพิจารณาโครงการ
เคลียร์สเปค
รอสำรวจหน้างาน
ติดตามแบบงาน
ระหว่างการเสนอราคา
รอลูกค้าออก PO
```
Visible only when:
```txt
Lead Status = Follow Up
```
---
# Scope D5.5 Lead Lost Reason
Master option:
```txt
crm_lead_lost_reason
```
Values:
```txt
price
delivery_time
sales_person
service
customer
product_quality
other
transfer
```
Display:
```txt
ราคา
ระยะเวลาจัดส่ง
พนักงานขาย
การบริการ
ลูกค้า
คุณภาพสินค้า
อื่นๆ
Transfer
```
Visible only when:
```txt
Lead Status = Closed Lost
or
Lead Status = Cancel
```
---
# Scope D5.6 Lead Outcome
Add:
```txt
crm_leads.outcome
```
Values:
```txt
open
won
lost
```
Purpose:
Marketing KPI and reporting.
Marketing must not update manually.
---
# Scope D5.7 Enquiry Refactor
Refactor:
```txt
crm_enquiries
```
to become a pure Sales entity.
Remove Lead lifecycle responsibilities.
Add:
```txt
leadId nullable
```
Relationship:
```txt
crm_leads 1
→ N crm_enquiries
```
---
# Scope D5.8 Lead Assignment
Replace:
```txt
pipelineStage change
```
with:
```txt
Create Enquiry
```
Flow:
```txt
Lead
Assign Sales
Create Enquiry
```
Lead remains.
Enquiry is created.
---
# Scope D5.9 Direct Sales Enquiry
Support:
```txt
Sales
Create Enquiry Directly
```
Business Rule:
```txt
leadId = null
```
Allowed.
Examples:
```txt
Existing Customer
Walk-in Customer
Direct Referral
Sales Prospecting
```
---
# Scope D5.10 Multiple Product Types
Support:
```txt
1 Lead
→ N Enquiries
```
Example:
```txt
Lead LD2606-001
├─ EN2606-001 Crane
├─ EN2606-002 Dock Door
└─ EN2606-003 Solar
```
Each enquiry has:
```txt
productTypeId
salesmanId
assignedToUserId
```
independently.
---
# Scope D5.11 Outcome Synchronization
Marketing should never update outcomes manually.
When:
```txt
Enquiry = Won
```
Update:
```txt
Lead Outcome = Won
```
Automatically.
---
When:
```txt
All Enquiries linked to Lead = Lost
```
Update:
```txt
Lead Outcome = Lost
```
Automatically.
---
Lead Outcome is derived from Enquiry outcomes.
---
# Scope D5.12 Document Sequences
Create dedicated sequences.
Lead:
```txt
LD2606-001
```
Enquiry:
```txt
EN2606-001
```
Separate document types.
Separate counters.
---
# Scope D5.13 Dashboard Refactor
Lead KPI Source:
```txt
crm_leads
```
Enquiry KPI Source:
```txt
crm_enquiries
```
No stage filtering.
---
# Scope D5.14 Reporting Refactor
Update:
* Lead Pipeline
* Lead Aging
* Lead Conversion
* Enquiry Pipeline
* Enquiry Aging
* Funnel Reports
Reuse:
```txt
Reporting Foundation
```
No parallel reporting implementation.
---
# Scope D5.15 Security Refactor
Lead visibility:
```txt
Marketing
Managers
Admins
```
Enquiry visibility:
```txt
Assigned Sales
Managers
Admins
```
Use:
```txt
resolveCrmMembershipAccess()
```
No direct role checks.
---
# Scope D5.16 Data Migration
Migrate existing records.
Rules:
```txt
pipelineStage = lead
→ crm_leads
pipelineStage = enquiry
→ crm_enquiries
pipelineStage = closed_won
→ crm_enquiries
pipelineStage = closed_lost
→ crm_enquiries
```
Preserve:
* follow-ups
* customers
* attachments
* ownership
* audit history
---
# Deliverables
## New Tables
* crm_leads
---
## Modified Tables
* crm_enquiries
---
## New Master Options
* crm_lead_awareness
* crm_lead_status
* crm_lead_followup_status
* crm_lead_lost_reason
---
## New APIs
* `/api/crm/leads`
* `/api/crm/leads/[id]`
* `/api/crm/leads/[id]/assign`
* `/api/crm/leads/[id]/followups`
---
## Updated APIs
* `/api/crm/enquiries`
* `/api/crm/dashboard`
* `/api/crm/reports`
---
## Documentation
Create:
```txt
docs/adr/0018-lead-enquiry-domain-separation.md
docs/implementation/task-d5-lead-enquiry-domain-separation.md
```
---
# Explicit Non-Scope
Do NOT:
* redesign quotation workflow
* redesign approval workflow
* redesign PDF workflow
* redesign role model
Reuse existing foundations.
---
# Verification
Verify:
```txt
1 Lead
→ N Enquiries
```
Verify:
```txt
1 Enquiry
→ N Quotations
```
Verify:
```txt
Sales Direct Enquiry
(leadId = null)
```
Verify:
```txt
Lead Outcome Auto Sync
```
Verify:
```txt
Lead Sequence
Enquiry Sequence
```
Verify:
```txt
Dashboard
Reports
Security
```
continue functioning.
Run:
```txt
npm exec tsc --noEmit
```
---
# Definition of Done
Task is complete when:
* Lead and Enquiry are separate entities
* Sales can create Enquiries directly
* Marketing can create Leads
* 1 Lead → N Enquiries works
* 1 Enquiry → N Quotations works
* Lead outcomes are automatically derived from Enquiry outcomes
* Lead and Enquiry document sequences are independent
* Dashboard and Reports use separated datasets
* Security follows existing CRM authorization foundation
Result:
Lead Domain = Established
Enquiry Domain = Established
Marketing Pipeline = Clear
Sales Pipeline = Clear

459
plans/task-ux.2.md Normal file
View File

@@ -0,0 +1,459 @@
# Task UX.2: CRM Form Design System Standardization
## Objective
Standardize all CRM form controls, layouts, field behaviors, and visual consistency across the entire ALLA CRM platform.
This task focuses on usability, consistency, accessibility, and maintainability.
The goal is to eliminate UI drift caused by different implementation patterns across Customer, Lead, Enquiry, Quotation, Approval, Settings, and Report modules.
This task creates the official CRM Form Design Foundation.
---
# Mandatory Review
Before implementation review:
* AGENTS.md
* docs/standards/ui-ux-rules.md
* docs/standards/project-foundations.md
* UX.1 Thai CRM Terminology Standardization
* Kiranism Dashboard Skill
* Existing shared form components
* Existing select-field implementation
* Existing date-picker implementation
* Existing currency/number inputs
---
# Required Skills
## Governance
* AGENTS.md
* Architecture Rules
* UI/UX Rules
* Task Review Checklist
---
## Frontend
* Kiranism Dashboard Skill
* Bento Grid Guidelines
* shadcn/ui Conventions
* React Hook Form Conventions
---
## CRM
* UX.1 Thai CRM Terminology
* Customer Foundation
* Lead / Enquiry Foundation
* Quotation Foundation
---
# Scope UX2.1 Form Control Inventory
Audit all CRM forms.
Identify:
* text inputs
* number inputs
* currency inputs
* percentage inputs
* date fields
* select fields
* textarea fields
* autocomplete fields
Document inconsistencies.
---
# Scope UX2.2 Standard Input Components
Create official CRM form standards.
## Text Input
Default:
```txt
w-full
```
Use for:
* names
* titles
* references
* remarks
---
## Number Input
Use:
```txt
type="number"
```
or dedicated number component.
No numeric values should use generic text input.
Examples:
* quantity
* chance %
* expected value
---
## Currency Input
Create a dedicated currency pattern.
Display:
```txt
1,234,567.89
```
Consistently throughout CRM.
Examples:
* quotation value
* budget estimate
* revenue
* PO amount
---
## Percentage Input
Display:
```txt
0%
25%
50%
75%
100%
```
Consistently.
---
# Scope UX2.3 Date Standardization
Replace inconsistent date inputs.
Standard:
```txt
Calendar Picker
```
Use for:
* quotation date
* enquiry date
* lead date
* follow-up date
* expected closing date
* PO date
---
Date format:
```txt
DD/MM/YYYY
```
System-wide.
---
# Scope UX2.4 Select Component Standardization
Fix oversized select controls.
Requirements:
```txt
w-full
max-w-full
truncate selected text
```
Dropdown content may be wider.
Trigger width must remain stable.
---
Fix known issue:
```txt
Project Parties
Customer Selection
Long Customer Names
```
must not stretch layout.
---
# Scope UX2.5 Form Grid Standardization
Desktop:
```txt
2-column layout
```
Default.
---
Mobile:
```txt
1-column layout
```
Default.
---
Examples:
```txt
Customer Name Customer Group
Customer Type Customer Sub Group
Phone Email
```
---
# Scope UX2.6 Required Field Standardization
All required fields must display a consistent indicator.
Example:
```txt
Customer Name *
```
or
```txt
Required Badge
```
Choose one pattern and apply globally.
---
# Scope UX2.7 Textarea Standardization
Textarea sizing rules:
```txt
Small
Medium
Large
```
Avoid arbitrary heights.
Examples:
* Remarks
* Exclusions
* Terms
* Internal Notes
---
# Scope UX2.8 Responsive Dialog Standards
Standardize:
* Sheet widths
* Dialog widths
* Form container widths
Prevent:
```txt
Overflow
Horizontal Scroll
Layout Shift
```
---
# Scope UX2.9 Report Filter Standardization
Apply same control standards to:
```txt
CRM Reports
Dashboard Filters
Analytics Filters
```
Reuse CRM form controls.
No custom filter controls.
---
# Scope UX2.10 Foundation Creation
Add to:
```txt
docs/standards/project-foundations.md
```
New Foundation:
```txt
CRM Form Design Foundation
```
Purpose:
Standardized CRM form controls and layouts.
---
# Deliverables
## New Documentation
* docs/implementation/task-ux2-crm-form-design-system-standardization.md
---
## Components Updated
* shared input controls
* shared select controls
* shared date controls
* shared number controls
* shared currency controls
---
## CRM Modules Updated
* Customers
* Leads
* Enquiries
* Quotations
* Approvals
* Settings
* Reports
where applicable.
---
# Explicit Non-Scope
Do NOT:
* redesign business workflows
* change CRM statuses
* change permissions
* change APIs
* change database schema
This task is UI/UX consistency only.
---
# Verification
Verify:
## Customer Form
* width consistency
* select consistency
* date consistency
---
## Lead Form
* responsive layout
* required fields
---
## Enquiry Form
* date picker behavior
---
## Quotation Form
* currency formatting
* project party selects
---
## Report Filters
* control consistency
---
Run:
```txt
npm exec tsc --noEmit
```
and verify visually.
---
# Definition of Done
Task is complete when:
* all CRM forms use the same control standards
* all date fields use the same date picker pattern
* all select fields behave consistently
* long values no longer break layouts
* form widths are standardized
* report filters follow the same design system
* CRM Form Design Foundation is documented
Result:
CRM Form Design System = ESTABLISHED
CRM UX Consistency = IMPROVED
Future CRM Forms = STANDARDIZED

View File

@@ -0,0 +1,55 @@
'use client';
import { useStore } from '@tanstack/react-form';
import { FieldDescription, FieldLabel } from '@/components/ui/field';
import {
useFieldContext,
FormFieldSet,
FormField,
FormFieldError,
createFormField
} from '@/components/ui/form-context';
import { CrmCurrencyInput } from '@/features/crm/components/crm-form-controls';
interface CurrencyFieldProps {
label: string;
description?: string;
required?: boolean;
currencyLabel?: string;
placeholder?: string;
}
export function CurrencyField({
label,
description,
required,
currencyLabel,
placeholder
}: CurrencyFieldProps) {
const field = useFieldContext();
const isTouched = useStore(field.store, (s) => s.meta.isTouched);
const isValid = useStore(field.store, (s) => s.meta.isValid);
const value = useStore(field.store, (s) => s.value) as string | number | undefined;
return (
<FormFieldSet>
<FormField>
<FieldLabel htmlFor={field.name}>
{label}
{required && ' *'}
</FieldLabel>
<CrmCurrencyInput
value={value === undefined || value === null ? '' : String(value)}
onChange={(nextValue) => field.handleChange(nextValue === '' ? '' : Number(nextValue))}
currencyLabel={currencyLabel}
placeholder={placeholder}
className={isTouched && !isValid ? 'border-destructive' : undefined}
/>
{description && <FieldDescription>{description}</FieldDescription>}
</FormField>
<FormFieldError />
</FormFieldSet>
);
}
export const FormCurrencyField = createFormField(CurrencyField);

View File

@@ -1,12 +1,7 @@
'use client'; 'use client';
import { useStore } from '@tanstack/react-form'; import { useStore } from '@tanstack/react-form';
import { format } from 'date-fns';
import { Calendar } from '@/components/ui/calendar';
import { FieldDescription, FieldLabel } from '@/components/ui/field'; import { FieldDescription, FieldLabel } from '@/components/ui/field';
import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { import {
FormField, FormField,
FormFieldError, FormFieldError,
@@ -14,7 +9,7 @@ import {
createFormField, createFormField,
useFieldContext useFieldContext
} from '@/components/ui/form-context'; } from '@/components/ui/form-context';
import { cn } from '@/lib/utils'; import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
interface DatePickerFieldProps { interface DatePickerFieldProps {
label: string; label: string;
@@ -28,51 +23,28 @@ export function DatePickerField({
label, label,
description, description,
required, required,
placeholder = 'Pick a date', placeholder = 'DD/MM/YYYY',
disabled disabled
}: DatePickerFieldProps) { }: DatePickerFieldProps) {
void disabled;
const field = useFieldContext(); const field = useFieldContext();
const value = useStore(field.store, (s) => s.value) as string | null | undefined; const value = useStore(field.store, (s) => s.value) as string | null | undefined;
const isTouched = useStore(field.store, (s) => s.meta.isTouched);
const isValid = useStore(field.store, (s) => s.meta.isValid);
const selectedDate = value ? new Date(value) : undefined;
return ( return (
<FormFieldSet> <FormFieldSet>
<FormField> <FormField>
<FieldLabel> <FieldLabel htmlFor={field.name}>
{label} {label}
{required && ' *'} {required && ' *'}
</FieldLabel> </FieldLabel>
<Popover> <CrmDateInput
<PopoverTrigger asChild> value={value}
<Button placeholder={placeholder}
type='button' onChange={(nextValue) => {
variant='outline' field.handleChange(nextValue);
className={cn(
'w-full justify-start text-left font-normal',
!value && 'text-muted-foreground'
)}
aria-invalid={isTouched && !isValid}
onBlur={field.handleBlur}
>
<Icons.calendar className='mr-2 h-4 w-4' />
{selectedDate ? format(selectedDate, 'PPP') : <span>{placeholder}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
<Calendar
mode='single'
selected={selectedDate}
onSelect={(date) => {
field.handleChange(date ? date.toISOString() : '');
field.handleBlur(); field.handleBlur();
}} }}
disabled={disabled}
initialFocus
/> />
</PopoverContent>
</Popover>
{description && <FieldDescription>{description}</FieldDescription>} {description && <FieldDescription>{description}</FieldDescription>}
</FormField> </FormField>
<FormFieldError /> <FormFieldError />

View File

@@ -8,6 +8,8 @@ export { RadioGroupField } from './radio-group-field';
export { SliderField } from './slider-field'; export { SliderField } from './slider-field';
export { FileUploadField } from './file-upload-field'; export { FileUploadField } from './file-upload-field';
export { DatePickerField } from './date-picker-field'; export { DatePickerField } from './date-picker-field';
export { CurrencyField } from './currency-field';
export { PercentageField } from './percentage-field';
// Composed (standalone, for direct use in forms) // Composed (standalone, for direct use in forms)
export { FormTextField } from './text-field'; export { FormTextField } from './text-field';
@@ -19,3 +21,5 @@ export { FormRadioGroupField } from './radio-group-field';
export { FormSliderField } from './slider-field'; export { FormSliderField } from './slider-field';
export { FormFileUploadField } from './file-upload-field'; export { FormFileUploadField } from './file-upload-field';
export { FormDatePickerField } from './date-picker-field'; export { FormDatePickerField } from './date-picker-field';
export { FormCurrencyField } from './currency-field';
export { FormPercentageField } from './percentage-field';

View File

@@ -0,0 +1,52 @@
'use client';
import { useStore } from '@tanstack/react-form';
import { FieldDescription, FieldLabel } from '@/components/ui/field';
import {
useFieldContext,
FormFieldSet,
FormField,
FormFieldError,
createFormField
} from '@/components/ui/form-context';
import { CrmPercentageInput } from '@/features/crm/components/crm-form-controls';
interface PercentageFieldProps {
label: string;
description?: string;
required?: boolean;
placeholder?: string;
}
export function PercentageField({
label,
description,
required,
placeholder
}: PercentageFieldProps) {
const field = useFieldContext();
const isTouched = useStore(field.store, (s) => s.meta.isTouched);
const isValid = useStore(field.store, (s) => s.meta.isValid);
const value = useStore(field.store, (s) => s.value) as string | number | undefined;
return (
<FormFieldSet>
<FormField>
<FieldLabel htmlFor={field.name}>
{label}
{required && ' *'}
</FieldLabel>
<CrmPercentageInput
value={value === undefined || value === null ? '' : String(value)}
onChange={(nextValue) => field.handleChange(nextValue === '' ? '' : Number(nextValue))}
placeholder={placeholder}
className={isTouched && !isValid ? 'border-destructive' : undefined}
/>
{description && <FieldDescription>{description}</FieldDescription>}
</FormField>
<FormFieldError />
</FormFieldSet>
);
}
export const FormPercentageField = createFormField(PercentageField);

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useStore } from '@tanstack/react-form'; import { useStore } from '@tanstack/react-form';
import { Textarea } from '@/components/ui/textarea';
import { FieldDescription, FieldLabel } from '@/components/ui/field'; import { FieldDescription, FieldLabel } from '@/components/ui/field';
import { import {
useFieldContext, useFieldContext,
@@ -10,6 +9,7 @@ import {
FormFieldError, FormFieldError,
createFormField createFormField
} from '@/components/ui/form-context'; } from '@/components/ui/form-context';
import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
interface TextareaFieldProps extends Omit< interface TextareaFieldProps extends Omit<
React.ComponentProps<'textarea'>, React.ComponentProps<'textarea'>,
@@ -20,6 +20,7 @@ interface TextareaFieldProps extends Omit<
required?: boolean; required?: boolean;
maxLength?: number; maxLength?: number;
showCount?: boolean; showCount?: boolean;
size?: 'sm' | 'md' | 'lg';
} }
export function TextareaField({ export function TextareaField({
@@ -28,6 +29,7 @@ export function TextareaField({
required, required,
maxLength, maxLength,
showCount = !!maxLength, showCount = !!maxLength,
size = 'md',
className, className,
...textareaProps ...textareaProps
}: TextareaFieldProps) { }: TextareaFieldProps) {
@@ -43,8 +45,9 @@ export function TextareaField({
{label} {label}
{required && ' *'} {required && ' *'}
</FieldLabel> </FieldLabel>
<Textarea <CrmTextarea
id={field.name} id={field.name}
size={size}
value={value} value={value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}

View File

@@ -31,7 +31,7 @@ function SelectTrigger({
data-slot='select-trigger' data-slot='select-trigger'
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full max-w-full min-w-0 items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:min-w-0 *:data-[slot=select-value]:flex-1 *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 *:data-[slot=select-value]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}

View File

@@ -29,6 +29,8 @@ import {
SliderField, SliderField,
FileUploadField, FileUploadField,
DatePickerField, DatePickerField,
CurrencyField,
PercentageField,
FormTextField, FormTextField,
FormTextareaField, FormTextareaField,
FormSelectField, FormSelectField,
@@ -37,7 +39,9 @@ import {
FormRadioGroupField, FormRadioGroupField,
FormSliderField, FormSliderField,
FormFileUploadField, FormFileUploadField,
FormDatePickerField FormDatePickerField,
FormCurrencyField,
FormPercentageField
} from '@/components/forms/fields'; } from '@/components/forms/fields';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@@ -152,7 +156,9 @@ const { useAppForm, withForm, withFieldGroup } = createFormHook({
RadioGroupField, RadioGroupField,
SliderField, SliderField,
FileUploadField, FileUploadField,
DatePickerField DatePickerField,
CurrencyField,
PercentageField
}, },
formComponents: { formComponents: {
// Layout & actions // Layout & actions
@@ -173,7 +179,9 @@ const { useAppForm, withForm, withFieldGroup } = createFormHook({
RadioGroupField: FormRadioGroupField, RadioGroupField: FormRadioGroupField,
SliderField: FormSliderField, SliderField: FormSliderField,
FileUploadField: FormFileUploadField, FileUploadField: FormFileUploadField,
DatePickerField: FormDatePickerField DatePickerField: FormDatePickerField,
CurrencyField: FormCurrencyField,
PercentageField: FormPercentageField
} }
}); });
@@ -208,7 +216,9 @@ function useFormFields<TValues extends Record<string, unknown>>() {
FormRadioGroupField: FormRadioGroupField as unknown as Typed<typeof FormRadioGroupField>, FormRadioGroupField: FormRadioGroupField as unknown as Typed<typeof FormRadioGroupField>,
FormSliderField: FormSliderField as unknown as Typed<typeof FormSliderField>, FormSliderField: FormSliderField as unknown as Typed<typeof FormSliderField>,
FormFileUploadField: FormFileUploadField as unknown as Typed<typeof FormFileUploadField>, FormFileUploadField: FormFileUploadField as unknown as Typed<typeof FormFileUploadField>,
FormDatePickerField: FormDatePickerField as unknown as Typed<typeof FormDatePickerField> FormDatePickerField: FormDatePickerField as unknown as Typed<typeof FormDatePickerField>,
FormCurrencyField: FormCurrencyField as unknown as Typed<typeof FormCurrencyField>,
FormPercentageField: FormPercentageField as unknown as Typed<typeof FormPercentageField>
}; };
} }

View File

@@ -0,0 +1,217 @@
'use client';
import * as React from 'react';
import { Calendar } from '@/components/ui/calendar';
import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
InputGroup,
InputGroupAddon,
InputGroupInput
} from '@/components/ui/input-group';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { formatCrmDate, formatCrmNumber, sanitizeDecimalInput } from '../shared/formats';
export const CRM_TEXTAREA_SIZE_CLASS = {
sm: 'min-h-20',
md: 'min-h-28',
lg: 'min-h-40'
} as const;
function parseDateValue(value: string | null | undefined) {
if (!value) {
return undefined;
}
const [year, month, day] = value.split('-').map(Number);
if (!year || !month || !day) {
return undefined;
}
return new Date(year, month - 1, day);
}
function toDateValue(date?: Date) {
if (!date) {
return '';
}
return [
String(date.getFullYear()),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0')
].join('-');
}
export function CrmDateInput({
value,
onChange,
placeholder = 'DD/MM/YYYY',
className
}: {
value?: string | null;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}) {
const selectedDate = parseDateValue(value);
return (
<Popover>
<PopoverTrigger asChild>
<Button
type='button'
variant='outline'
className={cn(
'w-full justify-start overflow-hidden text-left font-normal',
!value && 'text-muted-foreground',
className
)}
>
<Icons.calendar className='mr-2 h-4 w-4 shrink-0' />
<span className='truncate'>{value ? formatCrmDate(value) : placeholder}</span>
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
<Calendar
mode='single'
selected={selectedDate}
onSelect={(date) => onChange(toDateValue(date))}
initialFocus
/>
</PopoverContent>
</Popover>
);
}
function useFormattedNumericValue(value: string | number | null | undefined, options?: Intl.NumberFormatOptions) {
const [isFocused, setIsFocused] = React.useState(false);
const displayValue = React.useMemo(() => {
if (value === '' || value === null || value === undefined) {
return '';
}
return isFocused ? String(value).replaceAll(',', '') : formatCrmNumber(value, options);
}, [isFocused, options, value]);
return {
displayValue,
isFocused,
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false)
};
}
export function CrmNumberInput({
value,
onChange,
min,
max,
step,
placeholder,
className
}: {
value: string;
onChange: (value: string) => void;
min?: number;
max?: number;
step?: number | string;
placeholder?: string;
className?: string;
}) {
return (
<Input
type='number'
inputMode='decimal'
value={value}
min={min}
max={max}
step={step}
placeholder={placeholder}
className={cn('w-full', className)}
onChange={(event) => onChange(event.target.value)}
/>
);
}
export function CrmCurrencyInput({
value,
onChange,
currencyLabel = 'THB',
placeholder = '0.00',
className
}: {
value: string;
onChange: (value: string) => void;
currencyLabel?: string;
placeholder?: string;
className?: string;
}) {
const { displayValue, onBlur, onFocus } = useFormattedNumericValue(value, {
minimumFractionDigits: value ? 2 : 0,
maximumFractionDigits: 2
});
return (
<InputGroup className={className}>
<InputGroupAddon>{currencyLabel}</InputGroupAddon>
<InputGroupInput
type='text'
inputMode='decimal'
placeholder={placeholder}
value={displayValue}
onFocus={onFocus}
onBlur={onBlur}
onChange={(event) => onChange(sanitizeDecimalInput(event.target.value))}
/>
</InputGroup>
);
}
export function CrmPercentageInput({
value,
onChange,
placeholder = '0',
className
}: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}) {
return (
<InputGroup className={className}>
<InputGroupInput
type='number'
inputMode='decimal'
min={0}
max={100}
step='0.01'
placeholder={placeholder}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
<InputGroupAddon align='inline-end'>%</InputGroupAddon>
</InputGroup>
);
}
export function CrmTextarea({
size = 'md',
className,
...props
}: React.ComponentProps<typeof Textarea> & {
size?: keyof typeof CRM_TEXTAREA_SIZE_CLASS;
}) {
return (
<Textarea
className={cn(CRM_TEXTAREA_SIZE_CLASS[size], 'resize-y', className)}
{...props}
/>
);
}

View File

@@ -127,6 +127,7 @@ export function ContactFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='notes' name='notes'
size='md'
label='หมายเหตุ' label='หมายเหตุ'
placeholder='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม' placeholder='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม'
/> />

View File

@@ -20,7 +20,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
import { import {
shareCustomerContactMutation, shareCustomerContactMutation,
unshareCustomerContactMutation unshareCustomerContactMutation
@@ -131,10 +131,11 @@ export function ContactShareSheet({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Textarea <CrmTextarea
value={remark} value={remark}
onChange={(event) => setRemark(event.target.value)} onChange={(event) => setRemark(event.target.value)}
placeholder='Remark' placeholder='Remark'
size='md'
/> />
</div> </div>
) : null} ) : null}

View File

@@ -235,6 +235,7 @@ export function CustomerFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='notes' name='notes'
size='md'
label='หมายเหตุ' label='หมายเหตุ'
placeholder='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้' placeholder='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้'
/> />

View File

@@ -21,7 +21,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
import { assignCustomerOwnerMutation, clearCustomerOwnerMutation } from '../api/mutations'; import { assignCustomerOwnerMutation, clearCustomerOwnerMutation } from '../api/mutations';
import { customerByIdOptions } from '../api/queries'; import { customerByIdOptions } from '../api/queries';
import type { CustomerReferenceData } from '../api/types'; import type { CustomerReferenceData } from '../api/types';
@@ -156,10 +156,11 @@ export function CustomerOwnerCard({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Textarea <CrmTextarea
value={remark} value={remark}
onChange={(event) => setRemark(event.target.value)} onChange={(event) => setRemark(event.target.value)}
placeholder='Remark' placeholder='Remark'
size='md'
/> />
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -5,7 +5,6 @@ import Link from 'next/link';
import { parseAsString, useQueryStates } from 'nuqs'; import { parseAsString, useQueryStates } from 'nuqs';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -13,6 +12,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
import type { CrmDashboardReferenceData } from '../api/types'; import type { CrmDashboardReferenceData } from '../api/types';
export function DashboardFilters({ export function DashboardFilters({
@@ -47,15 +47,13 @@ export function DashboardFilters({
return ( return (
<div className='flex flex-col gap-4 rounded-xl border bg-card p-4'> <div className='flex flex-col gap-4 rounded-xl border bg-card p-4'>
<div className='grid gap-3 md:grid-cols-2 xl:grid-cols-6'> <div className='grid gap-3 md:grid-cols-2 xl:grid-cols-6'>
<Input <CrmDateInput
type='date'
value={params.dateFrom ?? ''} value={params.dateFrom ?? ''}
onChange={(event) => void setParams({ dateFrom: event.target.value || null })} onChange={(value) => void setParams({ dateFrom: value || null })}
/> />
<Input <CrmDateInput
type='date'
value={params.dateTo ?? ''} value={params.dateTo ?? ''}
onChange={(event) => void setParams({ dateTo: event.target.value || null })} onChange={(value) => void setParams({ dateTo: value || null })}
/> />
<Select <Select
value={params.branch ?? '__all__'} value={params.branch ?? '__all__'}

View File

@@ -1,15 +1,15 @@
'use client'; "use client";
import { useMemo } from 'react'; import { useMemo } from "react";
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'; import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from "@tanstack/react-query";
import { DataTable } from '@/components/ui/table/data-table'; import { DataTable } from "@/components/ui/table/data-table";
import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar'; import { DataTableToolbar } from "@/components/ui/table/data-table-toolbar";
import { useDataTable } from '@/hooks/use-data-table'; import { useDataTable } from "@/hooks/use-data-table";
import { getSortingStateParser } from '@/lib/parsers'; import { getSortingStateParser } from "@/lib/parsers";
import { enquiriesQueryOptions } from '../api/queries'; import { enquiriesQueryOptions } from "../api/queries";
import type { EnquiryReferenceData } from '../api/types'; import type { EnquiryReferenceData } from "../api/types";
import { getEnquiryColumns } from './enquiry-columns'; import { getEnquiryColumns } from "./enquiry-columns";
export function EnquiriesTable({ export function EnquiriesTable({
referenceData, referenceData,
@@ -17,20 +17,30 @@ export function EnquiriesTable({
canUpdate, canUpdate,
canDelete, canDelete,
canAssign, canAssign,
canReassign canReassign,
}: { }: {
referenceData: EnquiryReferenceData; referenceData: EnquiryReferenceData;
workspace: 'lead' | 'enquiry'; workspace: "lead" | "enquiry";
canUpdate: boolean; canUpdate: boolean;
canDelete: boolean; canDelete: boolean;
canAssign: boolean; canAssign: boolean;
canReassign: boolean; canReassign: boolean;
}) { }) {
const columns = useMemo( const columns = useMemo(
() => getEnquiryColumns({ referenceData, workspace, canUpdate, canDelete, canAssign, canReassign }), () =>
[referenceData, workspace, canUpdate, canDelete, canAssign, canReassign] getEnquiryColumns({
referenceData,
workspace,
canUpdate,
canDelete,
canAssign,
canReassign,
}),
[referenceData, workspace, canUpdate, canDelete, canAssign, canReassign],
); );
const columnIds = columns.map((column) => column.id).filter(Boolean) as string[]; const columnIds = columns
.map((column) => column.id)
.filter(Boolean) as string[];
const [params] = useQueryStates({ const [params] = useQueryStates({
page: parseAsInteger.withDefault(1), page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10), perPage: parseAsInteger.withDefault(10),
@@ -40,7 +50,7 @@ export function EnquiriesTable({
priority: parseAsString, priority: parseAsString,
branch: parseAsString, branch: parseAsString,
customer: parseAsString, customer: parseAsString,
sort: getSortingStateParser(columnIds).withDefault([]) sort: getSortingStateParser(columnIds).withDefault([]),
}); });
const filters = { const filters = {
@@ -53,10 +63,11 @@ export function EnquiriesTable({
...(params.priority && { priority: params.priority }), ...(params.priority && { priority: params.priority }),
...(params.branch && { branch: params.branch }), ...(params.branch && { branch: params.branch }),
...(params.customer && { customer: params.customer }), ...(params.customer && { customer: params.customer }),
...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) }) ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) }),
}; };
const { data } = useSuspenseQuery(enquiriesQueryOptions(filters)); const { data } = useSuspenseQuery(enquiriesQueryOptions(filters));
console.log("data", data);
const pageCount = Math.ceil(data.totalItems / params.perPage); const pageCount = Math.ceil(data.totalItems / params.perPage);
const { table } = useDataTable({ const { table } = useDataTable({
data: data.items, data: data.items,
@@ -65,8 +76,8 @@ export function EnquiriesTable({
shallow: true, shallow: true,
debounceMs: 500, debounceMs: 500,
initialState: { initialState: {
columnPinning: { right: ['actions'] } columnPinning: { right: ["actions"] },
} },
}); });
return ( return (

View File

@@ -81,8 +81,15 @@ export function EnquiryFormSheet({
); );
const [selectedCustomerId, setSelectedCustomerId] = useState(defaultValues.customerId); const [selectedCustomerId, setSelectedCustomerId] = useState(defaultValues.customerId);
const [projectParties, setProjectParties] = useState<ProjectPartyEditorItem[]>([]); const [projectParties, setProjectParties] = useState<ProjectPartyEditorItem[]>([]);
const { FormTextField, FormTextareaField, FormSelectField, FormSwitchField } = const {
useFormFields<EnquiryFormValues>(); FormTextField,
FormTextareaField,
FormSelectField,
FormSwitchField,
FormDatePickerField,
FormCurrencyField,
FormPercentageField
} = useFormFields<EnquiryFormValues>();
const projectPartiesQuery = useQuery({ const projectPartiesQuery = useQuery({
...enquiryProjectPartiesOptions(enquiry?.id ?? ''), ...enquiryProjectPartiesOptions(enquiry?.id ?? ''),
enabled: open && !!enquiry?.id enabled: open && !!enquiry?.id
@@ -354,22 +361,26 @@ export function EnquiryFormSheet({
label: item.label label: item.label
}))} }))}
/> />
<FormTextField <FormCurrencyField
name='estimatedValue' name='estimatedValue'
label='มูลค่าประมาณการ' label='มูลค่าประมาณการ'
type='number' currencyLabel='THB'
placeholder='2500000' placeholder='2500000'
/> />
<FormTextField name='chancePercent' label='โอกาสปิดการขาย %' type='number' placeholder='60' /> <FormPercentageField
<FormTextField name='chancePercent'
label='โอกาสปิดการขาย %'
placeholder='60'
/>
<FormDatePickerField
name='expectedCloseDate' name='expectedCloseDate'
label='วันที่คาดว่าจะปิดการขาย' label='วันที่คาดว่าจะปิดการขาย'
placeholder='YYYY-MM-DD'
/> />
<FormTextField name='competitor' label='คู่แข่ง' placeholder='Other vendor name' /> <FormTextField name='competitor' label='คู่แข่ง' placeholder='Other vendor name' />
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='description' name='description'
size='md'
label='รายละเอียด' label='รายละเอียด'
placeholder='สรุปภาพรวมของลีดหรือโอกาสขาย' placeholder='สรุปภาพรวมของลีดหรือโอกาสขาย'
/> />
@@ -377,6 +388,7 @@ export function EnquiryFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='requirement' name='requirement'
size='md'
label='ความต้องการ' label='ความต้องการ'
placeholder='ความต้องการเชิงเทคนิคหรือเชิงพาณิชย์' placeholder='ความต้องการเชิงเทคนิคหรือเชิงพาณิชย์'
/> />
@@ -384,6 +396,7 @@ export function EnquiryFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='notes' name='notes'
size='md'
label='หมายเหตุ' label='หมายเหตุ'
placeholder='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน' placeholder='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน'
/> />

View File

@@ -25,7 +25,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import {
CrmCurrencyInput,
CrmDateInput,
CrmTextarea
} from '@/features/crm/components/crm-form-controls';
import { import {
invalidateEnquiryMutationQueries, invalidateEnquiryMutationQueries,
markEnquiryLostMutation, markEnquiryLostMutation,
@@ -323,12 +327,16 @@ export function EnquiryOutcomeCard({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='po-date'>PO Date</Label> <Label htmlFor='po-date'>PO Date</Label>
<Input id='po-date' type='date' value={poDate} onChange={(e) => setPoDate(e.target.value)} /> <CrmDateInput value={poDate} onChange={setPoDate} className='w-full' />
</div> </div>
<div className='grid gap-4 md:grid-cols-2'> <div className='grid gap-4 md:grid-cols-2'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='po-amount'>PO Amount</Label> <Label htmlFor='po-amount'>PO Amount</Label>
<Input id='po-amount' value={poAmount} onChange={(e) => setPoAmount(e.target.value)} /> <CrmCurrencyInput
value={poAmount}
onChange={setPoAmount}
currencyLabel={poCurrency}
/>
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label>PO Currency</Label> <Label>PO Currency</Label>
@@ -346,7 +354,12 @@ export function EnquiryOutcomeCard({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='won-remark'>Remark</Label> <Label htmlFor='won-remark'>Remark</Label>
<Textarea id='won-remark' value={wonRemark} onChange={(e) => setWonRemark(e.target.value)} /> <CrmTextarea
id='won-remark'
value={wonRemark}
onChange={(e) => setWonRemark(e.target.value)}
size='md'
/>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -406,7 +419,12 @@ export function EnquiryOutcomeCard({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='lost-remark'>Lost Remark</Label> <Label htmlFor='lost-remark'>Lost Remark</Label>
<Textarea id='lost-remark' value={lostRemark} onChange={(e) => setLostRemark(e.target.value)} /> <CrmTextarea
id='lost-remark'
value={lostRemark}
onChange={(e) => setLostRemark(e.target.value)}
size='md'
/>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -441,10 +459,11 @@ export function EnquiryOutcomeCard({
</DialogHeader> </DialogHeader>
<div className='space-y-2 py-2'> <div className='space-y-2 py-2'>
<Label htmlFor='reopen-reason'>Reopen Reason</Label> <Label htmlFor='reopen-reason'>Reopen Reason</Label>
<Textarea <CrmTextarea
id='reopen-reason' id='reopen-reason'
value={reopenReason} value={reopenReason}
onChange={(e) => setReopenReason(e.target.value)} onChange={(e) => setReopenReason(e.target.value)}
size='md'
/> />
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -58,7 +58,8 @@ export function FollowupFormSheet({
}) { }) {
const isEdit = !!followup; const isEdit = !!followup;
const defaultValues = useMemo(() => toDefaultValues(followup), [followup]); const defaultValues = useMemo(() => toDefaultValues(followup), [followup]);
const { FormTextField, FormTextareaField } = useFormFields<EnquiryFollowupFormValues>(); const { FormTextField, FormTextareaField, FormDatePickerField } =
useFormFields<EnquiryFollowupFormValues>();
const contactOptions = referenceData.contacts.filter((item) => item.customerId === customerId); const contactOptions = referenceData.contacts.filter((item) => item.customerId === customerId);
const createMutation = useMutation({ const createMutation = useMutation({
@@ -136,11 +137,10 @@ export function FollowupFormSheet({
<div className='flex-1 overflow-auto'> <div className='flex-1 overflow-auto'>
<form.AppForm> <form.AppForm>
<form.Form id='enquiry-followup-form' className='grid gap-4 md:grid-cols-2'> <form.Form id='enquiry-followup-form' className='grid gap-4 md:grid-cols-2'>
<FormTextField <FormDatePickerField
name='followupDate' name='followupDate'
label='Follow-up Date' label='Follow-up Date'
required required
placeholder='YYYY-MM-DD'
/> />
<form.AppField <form.AppField
name='followupType' name='followupType'
@@ -204,10 +204,9 @@ export function FollowupFormSheet({
</field.FieldSet> </field.FieldSet>
)} )}
/> />
<FormTextField <FormDatePickerField
name='nextFollowupDate' name='nextFollowupDate'
label='Next Follow-up Date' label='Next Follow-up Date'
placeholder='YYYY-MM-DD'
/> />
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextField <FormTextField
@@ -219,6 +218,7 @@ export function FollowupFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='outcome' name='outcome'
size='md'
label='Outcome' label='Outcome'
placeholder='Customer asked for revised dimensions and a site survey' placeholder='Customer asked for revised dimensions and a site survey'
/> />
@@ -226,6 +226,7 @@ export function FollowupFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField <FormTextareaField
name='notes' name='notes'
size='md'
label='Notes' label='Notes'
placeholder='Internal notes from the follow-up session' placeholder='Internal notes from the follow-up session'
/> />

View File

@@ -6,7 +6,7 @@ import { toast } from 'sonner';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea'; import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
import { import {
approvalByIdOptions, approvalByIdOptions,
approvalsQueryOptions approvalsQueryOptions
@@ -80,11 +80,11 @@ export function QuotationApprovalTab({
{canSubmitNow ? ( {canSubmitNow ? (
<div className='space-y-3 rounded-lg border p-4'> <div className='space-y-3 rounded-lg border p-4'>
<div className='text-sm font-medium'>Submit quotation into approval flow</div> <div className='text-sm font-medium'>Submit quotation into approval flow</div>
<Textarea <CrmTextarea
value={remark} value={remark}
onChange={(event) => setRemark(event.target.value)} onChange={(event) => setRemark(event.target.value)}
placeholder='Optional remark for approvers' placeholder='Optional remark for approvers'
rows={3} size='md'
/> />
<Button <Button
isLoading={submitApproval.isPending} isLoading={submitApproval.isPending}

View File

@@ -33,7 +33,13 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea"; import {
CrmCurrencyInput,
CrmDateInput,
CrmNumberInput,
CrmPercentageInput,
CrmTextarea,
} from "@/features/crm/components/crm-form-controls";
import { import {
createQuotationAttachmentMutation, createQuotationAttachmentMutation,
createQuotationCustomerMutation, createQuotationCustomerMutation,
@@ -168,12 +174,11 @@ function ItemDialog({
placeholder="Description" placeholder="Description"
/> />
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<Input <CrmNumberInput
value={values.quantity} value={values.quantity}
onChange={(e) => onChange={(value) =>
setValues((s) => ({ ...s, quantity: e.target.value })) setValues((s) => ({ ...s, quantity: value }))
} }
type="number"
placeholder="Quantity" placeholder="Quantity"
/> />
<Select <Select
@@ -199,21 +204,21 @@ function ItemDialog({
</Select> </Select>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<Input <CrmCurrencyInput
value={values.unitPrice} value={values.unitPrice}
onChange={(e) => onChange={(value) =>
setValues((s) => ({ ...s, unitPrice: e.target.value })) setValues((s) => ({ ...s, unitPrice: value }))
} }
type="number"
placeholder="Unit price" placeholder="Unit price"
currencyLabel="THB"
/> />
<Input <CrmCurrencyInput
value={values.discount} value={values.discount}
onChange={(e) => onChange={(value) =>
setValues((s) => ({ ...s, discount: e.target.value })) setValues((s) => ({ ...s, discount: value }))
} }
type="number"
placeholder="Discount" placeholder="Discount"
currencyLabel="THB"
/> />
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
@@ -238,21 +243,21 @@ function ItemDialog({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Input <CrmPercentageInput
value={values.taxRate} value={values.taxRate}
onChange={(e) => onChange={(value) =>
setValues((s) => ({ ...s, taxRate: e.target.value })) setValues((s) => ({ ...s, taxRate: value }))
} }
type="number"
placeholder="Tax rate %" placeholder="Tax rate %"
/> />
</div> </div>
<Textarea <CrmTextarea
value={values.notes} value={values.notes}
onChange={(e) => onChange={(e) =>
setValues((s) => ({ ...s, notes: e.target.value })) setValues((s) => ({ ...s, notes: e.target.value }))
} }
placeholder="Notes" placeholder="Notes"
size="md"
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -418,11 +423,11 @@ function TopicDialog({
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Topic title" placeholder="Topic title"
/> />
<Textarea <CrmTextarea
value={itemsText} value={itemsText}
onChange={(e) => setItemsText(e.target.value)} onChange={(e) => setItemsText(e.target.value)}
placeholder="One line per topic item" placeholder="One line per topic item"
rows={6} size="lg"
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -500,12 +505,12 @@ function FollowupDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4"> <div className="grid gap-4">
<Input <CrmDateInput
type="date"
value={values.followupDate} value={values.followupDate}
onChange={(e) => onChange={(e) =>
setValues((s) => ({ ...s, followupDate: e.target.value })) setValues((s) => ({ ...s, followupDate: e }))
} }
className="w-full"
/> />
<Select <Select
value={values.followupType} value={values.followupType}
@@ -552,12 +557,12 @@ function FollowupDialog({
} }
placeholder="Outcome" placeholder="Outcome"
/> />
<Input <CrmDateInput
type="date"
value={values.nextFollowupDate} value={values.nextFollowupDate}
onChange={(e) => onChange={(e) =>
setValues((s) => ({ ...s, nextFollowupDate: e.target.value })) setValues((s) => ({ ...s, nextFollowupDate: e }))
} }
className="w-full"
/> />
<Input <Input
value={values.nextAction} value={values.nextAction}
@@ -566,12 +571,13 @@ function FollowupDialog({
} }
placeholder="Next action" placeholder="Next action"
/> />
<Textarea <CrmTextarea
value={values.notes} value={values.notes}
onChange={(e) => onChange={(e) =>
setValues((s) => ({ ...s, notes: e.target.value })) setValues((s) => ({ ...s, notes: e.target.value }))
} }
placeholder="Notes" placeholder="Notes"
size="md"
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -668,21 +674,21 @@ function AttachmentDialog({
} }
placeholder="application/pdf" placeholder="application/pdf"
/> />
<Input <CrmNumberInput
value={values.fileSize} value={values.fileSize}
onChange={(e) => onChange={(value) =>
setValues((s) => ({ ...s, fileSize: e.target.value })) setValues((s) => ({ ...s, fileSize: value }))
} }
type="number"
placeholder="File size bytes" placeholder="File size bytes"
/> />
</div> </div>
<Textarea <CrmTextarea
value={values.description} value={values.description}
onChange={(e) => onChange={(e) =>
setValues((s) => ({ ...s, description: e.target.value })) setValues((s) => ({ ...s, description: e.target.value }))
} }
placeholder="Description" placeholder="Description"
size="md"
/> />
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -10,6 +10,13 @@ import {
ProjectPartiesEditor, ProjectPartiesEditor,
type ProjectPartyEditorItem type ProjectPartyEditorItem
} from '@/features/crm/components/project-parties-editor'; } from '@/features/crm/components/project-parties-editor';
import {
CrmCurrencyInput,
CrmDateInput,
CrmNumberInput,
CrmPercentageInput,
CrmTextarea
} from '@/features/crm/components/crm-form-controls';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
Select, Select,
@@ -27,7 +34,6 @@ import {
SheetTitle SheetTitle
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { quotationCustomersOptions } from '../api/queries'; import { quotationCustomersOptions } from '../api/queries';
import { createQuotationMutation, updateQuotationMutation } from '../api/mutations'; import { createQuotationMutation, updateQuotationMutation } from '../api/mutations';
import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types'; import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types';
@@ -99,16 +105,21 @@ function toFormState(
function Field({ function Field({
label, label,
required = false,
children, children,
className className
}: { }: {
label: string; label: string;
required?: boolean;
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}) { }) {
return ( return (
<div className={className}> <div className={className}>
<div className='mb-2 text-sm font-medium'>{label}</div> <div className='mb-2 text-sm font-medium'>
{label}
{required ? ' *' : ''}
</div>
{children} {children}
</div> </div>
); );
@@ -308,7 +319,7 @@ export function QuotationFormSheet({
</Select> </Select>
</Field> </Field>
<Field label='Quotation Type'> <Field label='Quotation Type' required>
<Select value={state.quotationType} onValueChange={(value) => setField('quotationType', value)}> <Select value={state.quotationType} onValueChange={(value) => setField('quotationType', value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder='Select type' /> <SelectValue placeholder='Select type' />
@@ -323,12 +334,12 @@ export function QuotationFormSheet({
</Select> </Select>
</Field> </Field>
<Field label='Quotation Date'> <Field label='Quotation Date' required>
<Input type='date' value={state.quotationDate} onChange={(e) => setField('quotationDate', e.target.value)} /> <CrmDateInput value={state.quotationDate} onChange={(value) => setField('quotationDate', value)} />
</Field> </Field>
<Field label='Valid Until'> <Field label='Valid Until'>
<Input type='date' value={state.validUntil} onChange={(e) => setField('validUntil', e.target.value)} /> <CrmDateInput value={state.validUntil} onChange={(value) => setField('validUntil', value)} />
</Field> </Field>
<Field label='Branch'> <Field label='Branch'>
@@ -378,7 +389,7 @@ export function QuotationFormSheet({
</Field> </Field>
<Field label='Exchange Rate'> <Field label='Exchange Rate'>
<Input value={state.exchangeRate} onChange={(e) => setField('exchangeRate', e.target.value)} type='number' step='0.0001' /> <CrmNumberInput value={state.exchangeRate} onChange={(value) => setField('exchangeRate', value)} step='0.0001' />
</Field> </Field>
<Field label='Project Name'> <Field label='Project Name'>
@@ -414,11 +425,11 @@ export function QuotationFormSheet({
</Field> </Field>
<Field label='Chance %'> <Field label='Chance %'>
<Input value={state.chancePercent} onChange={(e) => setField('chancePercent', e.target.value)} type='number' min='0' max='100' /> <CrmPercentageInput value={state.chancePercent} onChange={(value) => setField('chancePercent', value)} />
</Field> </Field>
<Field label='Discount'> <Field label='Discount'>
<Input value={state.discount} onChange={(e) => setField('discount', e.target.value)} type='number' step='0.01' /> <CrmCurrencyInput value={state.discount} onChange={(value) => setField('discount', value)} currencyLabel='THB' />
</Field> </Field>
<Field label='Discount Type'> <Field label='Discount Type'>
@@ -438,7 +449,7 @@ export function QuotationFormSheet({
</Field> </Field>
<Field label='Tax Rate %'> <Field label='Tax Rate %'>
<Input value={state.taxRate} onChange={(e) => setField('taxRate', e.target.value)} type='number' step='0.01' /> <CrmPercentageInput value={state.taxRate} onChange={(value) => setField('taxRate', value)} />
</Field> </Field>
<Field label='Sent Via'> <Field label='Sent Via'>
@@ -472,13 +483,13 @@ export function QuotationFormSheet({
<div className='md:col-span-2'> <div className='md:col-span-2'>
<Field label='Notes'> <Field label='Notes'>
<Textarea value={state.notes} onChange={(e) => setField('notes', e.target.value)} placeholder='Internal notes' /> <CrmTextarea value={state.notes} onChange={(e) => setField('notes', e.target.value)} placeholder='Internal notes' size='md' />
</Field> </Field>
</div> </div>
<div className='md:col-span-2'> <div className='md:col-span-2'>
<Field label='Revision Remark'> <Field label='Revision Remark'>
<Textarea value={state.revisionRemark} onChange={(e) => setField('revisionRemark', e.target.value)} placeholder='Revision note or approval context' /> <CrmTextarea value={state.revisionRemark} onChange={(e) => setField('revisionRemark', e.target.value)} placeholder='Revision note or approval context' size='md' />
</Field> </Field>
</div> </div>

View File

@@ -5,7 +5,6 @@ import Link from 'next/link';
import { parseAsString, useQueryStates } from 'nuqs'; import { parseAsString, useQueryStates } from 'nuqs';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -13,6 +12,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
import type { CrmReportFilterMetadata } from '../api/types'; import type { CrmReportFilterMetadata } from '../api/types';
type FilterKey = type FilterKey =
@@ -92,17 +92,15 @@ export function ReportFilterBar({
<div className='grid gap-3 md:grid-cols-2 xl:grid-cols-6'> <div className='grid gap-3 md:grid-cols-2 xl:grid-cols-6'>
{filterKeys.includes('dateFrom') ? ( {filterKeys.includes('dateFrom') ? (
<Input <CrmDateInput
type='date'
value={params.dateFrom ?? ''} value={params.dateFrom ?? ''}
onChange={(event) => void setParams({ dateFrom: event.target.value || null })} onChange={(value) => void setParams({ dateFrom: value || null })}
/> />
) : null} ) : null}
{filterKeys.includes('dateTo') ? ( {filterKeys.includes('dateTo') ? (
<Input <CrmDateInput
type='date'
value={params.dateTo ?? ''} value={params.dateTo ?? ''}
onChange={(event) => void setParams({ dateTo: event.target.value || null })} onChange={(value) => void setParams({ dateTo: value || null })}
/> />
) : null} ) : null}
{( {(

View File

@@ -0,0 +1,47 @@
import { format, isValid, parse, parseISO } from 'date-fns';
function coerceDate(value: string | Date | null | undefined) {
if (!value) {
return null;
}
if (value instanceof Date) {
return isValid(value) ? value : null;
}
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
const parsed = parse(value, 'yyyy-MM-dd', new Date());
return isValid(parsed) ? parsed : null;
}
const isoParsed = parseISO(value);
return isValid(isoParsed) ? isoParsed : null;
}
export function formatCrmDate(value: string | Date | null | undefined) {
const date = coerceDate(value);
return date ? format(date, 'dd/MM/yyyy') : '';
}
export function formatCrmNumber(value: number | string | null | undefined, options?: Intl.NumberFormatOptions) {
if (value === '' || value === null || value === undefined) {
return '';
}
const parsed = typeof value === 'number' ? value : Number(String(value).replaceAll(',', ''));
if (Number.isNaN(parsed)) {
return '';
}
return new Intl.NumberFormat('en-US', options).format(parsed);
}
export function sanitizeDecimalInput(value: string) {
const normalized = value.replaceAll(',', '').replace(/[^\d.-]/g, '');
const [head = '', ...tail] = normalized.split('.');
const integerPart = head.replace(/(?!^)-/g, '');
const decimalPart = tail.join('');
return decimalPart.length > 0 ? `${integerPart}.${decimalPart}` : integerPart;
}