commit
This commit is contained in:
135
AGENTS.md
135
AGENTS.md
@@ -34,6 +34,127 @@ Current migration status:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Governance Layer
|
||||||
|
|
||||||
|
This repository now treats governance documentation as implementation input, not optional background reading.
|
||||||
|
|
||||||
|
Before any task that changes behavior, architecture, permissions, reporting, exports, PDFs, approvals, or CRM workflow, agents must review the relevant standards and foundations first.
|
||||||
|
|
||||||
|
Primary governance documents:
|
||||||
|
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `docs/standards/task-contract-template.md`
|
||||||
|
- `docs/standards/task-catalog.md`
|
||||||
|
- `docs/standards/project-foundations.md`
|
||||||
|
- `docs/standards/architecture-rules.md`
|
||||||
|
- `docs/standards/ui-ux-rules.md`
|
||||||
|
- `docs/standards/task-review-checklist.md`
|
||||||
|
- `docs/adr/**`
|
||||||
|
- `docs/business/**`
|
||||||
|
- `docs/security/**`
|
||||||
|
- `docs/implementation/**` for the related feature lineage
|
||||||
|
|
||||||
|
### Task Execution Rules
|
||||||
|
|
||||||
|
All tasks follow these rules:
|
||||||
|
|
||||||
|
- review first; implementation without review is prohibited
|
||||||
|
- reuse existing foundations before creating new modules, helpers, or flows
|
||||||
|
- extend existing services before creating duplicate services
|
||||||
|
- extend existing route groups before creating duplicate APIs
|
||||||
|
- extend existing permission models before creating duplicate permission systems
|
||||||
|
- extend existing export/report foundations before creating duplicate export flows
|
||||||
|
- keep business logic in feature service layers, not in route handlers or client components
|
||||||
|
- document any exception when an existing foundation cannot be reused
|
||||||
|
|
||||||
|
Required review order for non-trivial work:
|
||||||
|
|
||||||
|
1. `AGENTS.md`
|
||||||
|
2. `docs/standards/**`
|
||||||
|
3. relevant `docs/adr/**`
|
||||||
|
4. relevant `docs/business/**`
|
||||||
|
5. related completed tasks from `docs/standards/task-catalog.md` and `docs/implementation/**`
|
||||||
|
6. existing foundations under `src/features/foundation/**`
|
||||||
|
7. existing feature implementations under `src/features/**`
|
||||||
|
8. existing APIs under `src/app/api/**`
|
||||||
|
9. permission and access enforcement under `src/lib/auth/**`, `src/features/crm/security/**`, and `docs/security/**`
|
||||||
|
10. existing audit patterns under `src/features/foundation/audit-log/**`
|
||||||
|
|
||||||
|
### Historical Knowledge Rule
|
||||||
|
|
||||||
|
Before implementing any feature:
|
||||||
|
|
||||||
|
1. review related foundations
|
||||||
|
2. review related ADRs
|
||||||
|
3. review related completed tasks from `docs/standards/task-catalog.md`
|
||||||
|
4. reuse before creating
|
||||||
|
|
||||||
|
Implementation without historical review is prohibited.
|
||||||
|
|
||||||
|
### Required Foundations
|
||||||
|
|
||||||
|
The following reusable foundations are mandatory whenever the task touches their area:
|
||||||
|
|
||||||
|
- Audit Foundation: `src/features/foundation/audit-log/**`
|
||||||
|
- Approval Foundation: `src/features/foundation/approval/**`
|
||||||
|
- Storage Foundation: `src/features/foundation/storage/**` and `src/features/foundation/document-artifact/**`
|
||||||
|
- PDF Foundation: `src/features/foundation/pdf-generator/**` and `src/features/crm/quotations/document/**`
|
||||||
|
- Report Foundation: `src/features/crm/reports/**`, `docs/adr/0017-report-foundation.md`
|
||||||
|
- CRM Authorization Foundation: `src/lib/auth/crm-access.ts`, `src/features/crm/security/**`, `docs/security/**`
|
||||||
|
- Customer Ownership Foundation: `docs/adr/0015-customer-ownership-contact-sharing.md`, `src/features/crm/customers/**`
|
||||||
|
- Contact Sharing Foundation: `docs/adr/0015-customer-ownership-contact-sharing.md`, `src/app/api/crm/customers/[id]/contacts/[contactId]/shares/**`
|
||||||
|
|
||||||
|
If a task touches CRM leads, enquiries, quotations, approvals, reports, exports, storage, or PDFs, agents must explicitly check whether one of these foundations already solves part of the problem.
|
||||||
|
|
||||||
|
### Architecture Constraints
|
||||||
|
|
||||||
|
Frontend constraints:
|
||||||
|
|
||||||
|
- Next.js App Router only
|
||||||
|
- server components first
|
||||||
|
- `PageContainer` for dashboard page headers
|
||||||
|
- shadcn/ui primitives and existing app UI wrappers
|
||||||
|
- TanStack React Query with server prefetch + `HydrationBoundary` + client `useSuspenseQuery()` for data-heavy app pages
|
||||||
|
- TanStack Form + Zod for forms
|
||||||
|
- `nuqs` for URL state
|
||||||
|
- existing CRM terminology helpers and Thai labels for business-facing CRM UI
|
||||||
|
|
||||||
|
Backend constraints:
|
||||||
|
|
||||||
|
- Route Handlers are the main HTTP boundary
|
||||||
|
- feature service layer owns business logic
|
||||||
|
- Drizzle ORM owns database access
|
||||||
|
- auth and organization access go through `@/auth` and `src/lib/auth/session.ts`
|
||||||
|
- CRM scope resolution goes through resolved-access helpers, not direct role-string branching
|
||||||
|
- audit logging goes through `src/features/foundation/audit-log/service.ts`
|
||||||
|
|
||||||
|
Forbidden patterns:
|
||||||
|
|
||||||
|
- Redux for new application state
|
||||||
|
- SWR for new server-state fetching
|
||||||
|
- React Hook Form for new forms in this repo
|
||||||
|
- direct database access from client components
|
||||||
|
- business logic inside route handlers
|
||||||
|
- new Clerk usage
|
||||||
|
- direct role-string authorization such as `if (role === 'sales')`
|
||||||
|
- report-specific export systems that bypass `src/features/crm/reports/server/exports/service.ts`
|
||||||
|
|
||||||
|
### Mandatory Task Review
|
||||||
|
|
||||||
|
Before implementation, every non-trivial task must review:
|
||||||
|
|
||||||
|
- reusable foundations
|
||||||
|
- relevant ADRs
|
||||||
|
- related completed tasks from `docs/standards/task-catalog.md`
|
||||||
|
- related existing APIs
|
||||||
|
- permission and scope enforcement
|
||||||
|
- audit events and existing entity/action naming
|
||||||
|
- duplication risk across services, routes, exports, datasets, and UI shells
|
||||||
|
|
||||||
|
Implementation without this review is prohibited.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Technology Stack Details
|
## 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
160
docs/standards/architecture-rules.md
Normal file
160
docs/standards/architecture-rules.md
Normal 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
|
||||||
364
docs/standards/project-foundations.md
Normal file
364
docs/standards/project-foundations.md
Normal 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
|
||||||
622
docs/standards/task-catalog.md
Normal file
622
docs/standards/task-catalog.md
Normal 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
|
||||||
148
docs/standards/task-contract-template.md
Normal file
148
docs/standards/task-contract-template.md
Normal 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
|
||||||
82
docs/standards/task-review-checklist.md
Normal file
82
docs/standards/task-review-checklist.md
Normal 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
|
||||||
111
docs/standards/ui-ux-rules.md
Normal file
111
docs/standards/ui-ux-rules.md
Normal 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
766
plans/task-d.5.md
Normal 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
459
plans/task-ux.2.md
Normal 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
|
||||||
55
src/components/forms/fields/currency-field.tsx
Normal file
55
src/components/forms/fields/currency-field.tsx
Normal 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);
|
||||||
@@ -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(
|
field.handleBlur();
|
||||||
'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();
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{description && <FieldDescription>{description}</FieldDescription>}
|
{description && <FieldDescription>{description}</FieldDescription>}
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormFieldError />
|
<FormFieldError />
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
52
src/components/forms/fields/percentage-field.tsx
Normal file
52
src/components/forms/fields/percentage-field.tsx
Normal 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);
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
217
src/features/crm/components/crm-form-controls.tsx
Normal file
217
src/features/crm/components/crm-form-controls.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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__'}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
{(
|
{(
|
||||||
|
|||||||
47
src/features/crm/shared/formats.ts
Normal file
47
src/features/crm/shared/formats.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user