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
|
||||
|
||||
### 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.
|
||||
18. If a component overrides `useMutation` callbacks on top of a shared mutation config, preserve or explicitly call the shared invalidation behavior before closing UI.
|
||||
19. After successful create/update/delete, sheets, dialogs, and destructive modals must close only after cache invalidation has been triggered and the UI can refresh from fresh data.
|
||||
20. Review `docs/standards/**`, relevant `docs/adr/**`, `docs/business/**`, and `docs/security/**` before implementing non-trivial changes.
|
||||
21. Review related completed task notes in `docs/standards/task-catalog.md` and `docs/implementation/**` before implementing non-trivial changes.
|
||||
22. Reuse foundations under `src/features/foundation/**` and `src/features/crm/**` before introducing new services, routes, permissions, exports, or datasets.
|
||||
23. Do not create duplicate report/export/PDF/approval/security plumbing when a project foundation already exists.
|
||||
24. CRM authorization must flow through resolved access helpers such as `resolveCrmMembershipAccess()`, `buildCrmSecurityContext()`, or report-context builders; do not authorize with raw role strings alone.
|
||||
|
||||
Before implementing any task, review:
|
||||
- AGENTS.md
|
||||
- docs/standards/*
|
||||
- docs/standards/task-catalog.md
|
||||
- related ADRs
|
||||
- related completed task documents
|
||||
|
||||
Implementation without review is prohibited.
|
||||
|
||||
@@ -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';
|
||||
|
||||
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 { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import {
|
||||
FormField,
|
||||
FormFieldError,
|
||||
@@ -14,7 +9,7 @@ import {
|
||||
createFormField,
|
||||
useFieldContext
|
||||
} from '@/components/ui/form-context';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
|
||||
|
||||
interface DatePickerFieldProps {
|
||||
label: string;
|
||||
@@ -28,51 +23,28 @@ export function DatePickerField({
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
placeholder = 'Pick a date',
|
||||
placeholder = 'DD/MM/YYYY',
|
||||
disabled
|
||||
}: DatePickerFieldProps) {
|
||||
void disabled;
|
||||
const field = useFieldContext();
|
||||
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 (
|
||||
<FormFieldSet>
|
||||
<FormField>
|
||||
<FieldLabel>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{label}
|
||||
{required && ' *'}
|
||||
</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground'
|
||||
)}
|
||||
aria-invalid={isTouched && !isValid}
|
||||
onBlur={field.handleBlur}
|
||||
>
|
||||
<Icons.calendar className='mr-2 h-4 w-4' />
|
||||
{selectedDate ? format(selectedDate, 'PPP') : <span>{placeholder}</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0' align='start'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={selectedDate}
|
||||
onSelect={(date) => {
|
||||
field.handleChange(date ? date.toISOString() : '');
|
||||
<CrmDateInput
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(nextValue) => {
|
||||
field.handleChange(nextValue);
|
||||
field.handleBlur();
|
||||
}}
|
||||
disabled={disabled}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{description && <FieldDescription>{description}</FieldDescription>}
|
||||
</FormField>
|
||||
<FormFieldError />
|
||||
|
||||
@@ -8,6 +8,8 @@ export { RadioGroupField } from './radio-group-field';
|
||||
export { SliderField } from './slider-field';
|
||||
export { FileUploadField } from './file-upload-field';
|
||||
export { DatePickerField } from './date-picker-field';
|
||||
export { CurrencyField } from './currency-field';
|
||||
export { PercentageField } from './percentage-field';
|
||||
|
||||
// Composed (standalone, for direct use in forms)
|
||||
export { FormTextField } from './text-field';
|
||||
@@ -19,3 +21,5 @@ export { FormRadioGroupField } from './radio-group-field';
|
||||
export { FormSliderField } from './slider-field';
|
||||
export { FormFileUploadField } from './file-upload-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';
|
||||
|
||||
import { useStore } from '@tanstack/react-form';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { FieldDescription, FieldLabel } from '@/components/ui/field';
|
||||
import {
|
||||
useFieldContext,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
FormFieldError,
|
||||
createFormField
|
||||
} from '@/components/ui/form-context';
|
||||
import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
|
||||
|
||||
interface TextareaFieldProps extends Omit<
|
||||
React.ComponentProps<'textarea'>,
|
||||
@@ -20,6 +20,7 @@ interface TextareaFieldProps extends Omit<
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
showCount?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function TextareaField({
|
||||
@@ -28,6 +29,7 @@ export function TextareaField({
|
||||
required,
|
||||
maxLength,
|
||||
showCount = !!maxLength,
|
||||
size = 'md',
|
||||
className,
|
||||
...textareaProps
|
||||
}: TextareaFieldProps) {
|
||||
@@ -43,8 +45,9 @@ export function TextareaField({
|
||||
{label}
|
||||
{required && ' *'}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
id={field.name}
|
||||
size={size}
|
||||
value={value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
|
||||
@@ -31,7 +31,7 @@ function SelectTrigger({
|
||||
data-slot='select-trigger'
|
||||
data-size={size}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
SliderField,
|
||||
FileUploadField,
|
||||
DatePickerField,
|
||||
CurrencyField,
|
||||
PercentageField,
|
||||
FormTextField,
|
||||
FormTextareaField,
|
||||
FormSelectField,
|
||||
@@ -37,7 +39,9 @@ import {
|
||||
FormRadioGroupField,
|
||||
FormSliderField,
|
||||
FormFileUploadField,
|
||||
FormDatePickerField
|
||||
FormDatePickerField,
|
||||
FormCurrencyField,
|
||||
FormPercentageField
|
||||
} from '@/components/forms/fields';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@@ -152,7 +156,9 @@ const { useAppForm, withForm, withFieldGroup } = createFormHook({
|
||||
RadioGroupField,
|
||||
SliderField,
|
||||
FileUploadField,
|
||||
DatePickerField
|
||||
DatePickerField,
|
||||
CurrencyField,
|
||||
PercentageField
|
||||
},
|
||||
formComponents: {
|
||||
// Layout & actions
|
||||
@@ -173,7 +179,9 @@ const { useAppForm, withForm, withFieldGroup } = createFormHook({
|
||||
RadioGroupField: FormRadioGroupField,
|
||||
SliderField: FormSliderField,
|
||||
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>,
|
||||
FormSliderField: FormSliderField as unknown as Typed<typeof FormSliderField>,
|
||||
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'>
|
||||
<FormTextareaField
|
||||
name='notes'
|
||||
size='md'
|
||||
label='หมายเหตุ'
|
||||
placeholder='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม'
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
|
||||
import {
|
||||
shareCustomerContactMutation,
|
||||
unshareCustomerContactMutation
|
||||
@@ -131,10 +131,11 @@ export function ContactShareSheet({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={remark}
|
||||
onChange={(event) => setRemark(event.target.value)}
|
||||
placeholder='Remark'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -235,6 +235,7 @@ export function CustomerFormSheet({
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='notes'
|
||||
size='md'
|
||||
label='หมายเหตุ'
|
||||
placeholder='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้'
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} 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 { customerByIdOptions } from '../api/queries';
|
||||
import type { CustomerReferenceData } from '../api/types';
|
||||
@@ -156,10 +156,11 @@ export function CustomerOwnerCard({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={remark}
|
||||
onChange={(event) => setRemark(event.target.value)}
|
||||
placeholder='Remark'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Link from 'next/link';
|
||||
import { parseAsString, useQueryStates } from 'nuqs';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
|
||||
import type { CrmDashboardReferenceData } from '../api/types';
|
||||
|
||||
export function DashboardFilters({
|
||||
@@ -47,15 +47,13 @@ export function DashboardFilters({
|
||||
return (
|
||||
<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'>
|
||||
<Input
|
||||
type='date'
|
||||
<CrmDateInput
|
||||
value={params.dateFrom ?? ''}
|
||||
onChange={(event) => void setParams({ dateFrom: event.target.value || null })}
|
||||
onChange={(value) => void setParams({ dateFrom: value || null })}
|
||||
/>
|
||||
<Input
|
||||
type='date'
|
||||
<CrmDateInput
|
||||
value={params.dateTo ?? ''}
|
||||
onChange={(event) => void setParams({ dateTo: event.target.value || null })}
|
||||
onChange={(value) => void setParams({ dateTo: value || null })}
|
||||
/>
|
||||
<Select
|
||||
value={params.branch ?? '__all__'}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { DataTable } from '@/components/ui/table/data-table';
|
||||
import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar';
|
||||
import { useDataTable } from '@/hooks/use-data-table';
|
||||
import { getSortingStateParser } from '@/lib/parsers';
|
||||
import { enquiriesQueryOptions } from '../api/queries';
|
||||
import type { EnquiryReferenceData } from '../api/types';
|
||||
import { getEnquiryColumns } from './enquiry-columns';
|
||||
import { useMemo } from "react";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { DataTable } from "@/components/ui/table/data-table";
|
||||
import { DataTableToolbar } from "@/components/ui/table/data-table-toolbar";
|
||||
import { useDataTable } from "@/hooks/use-data-table";
|
||||
import { getSortingStateParser } from "@/lib/parsers";
|
||||
import { enquiriesQueryOptions } from "../api/queries";
|
||||
import type { EnquiryReferenceData } from "../api/types";
|
||||
import { getEnquiryColumns } from "./enquiry-columns";
|
||||
|
||||
export function EnquiriesTable({
|
||||
referenceData,
|
||||
@@ -17,20 +17,30 @@ export function EnquiriesTable({
|
||||
canUpdate,
|
||||
canDelete,
|
||||
canAssign,
|
||||
canReassign
|
||||
canReassign,
|
||||
}: {
|
||||
referenceData: EnquiryReferenceData;
|
||||
workspace: 'lead' | 'enquiry';
|
||||
workspace: "lead" | "enquiry";
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
canAssign: boolean;
|
||||
canReassign: boolean;
|
||||
}) {
|
||||
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({
|
||||
page: parseAsInteger.withDefault(1),
|
||||
perPage: parseAsInteger.withDefault(10),
|
||||
@@ -40,7 +50,7 @@ export function EnquiriesTable({
|
||||
priority: parseAsString,
|
||||
branch: parseAsString,
|
||||
customer: parseAsString,
|
||||
sort: getSortingStateParser(columnIds).withDefault([])
|
||||
sort: getSortingStateParser(columnIds).withDefault([]),
|
||||
});
|
||||
|
||||
const filters = {
|
||||
@@ -53,10 +63,11 @@ export function EnquiriesTable({
|
||||
...(params.priority && { priority: params.priority }),
|
||||
...(params.branch && { branch: params.branch }),
|
||||
...(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));
|
||||
console.log("data", data);
|
||||
const pageCount = Math.ceil(data.totalItems / params.perPage);
|
||||
const { table } = useDataTable({
|
||||
data: data.items,
|
||||
@@ -65,8 +76,8 @@ export function EnquiriesTable({
|
||||
shallow: true,
|
||||
debounceMs: 500,
|
||||
initialState: {
|
||||
columnPinning: { right: ['actions'] }
|
||||
}
|
||||
columnPinning: { right: ["actions"] },
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -81,8 +81,15 @@ export function EnquiryFormSheet({
|
||||
);
|
||||
const [selectedCustomerId, setSelectedCustomerId] = useState(defaultValues.customerId);
|
||||
const [projectParties, setProjectParties] = useState<ProjectPartyEditorItem[]>([]);
|
||||
const { FormTextField, FormTextareaField, FormSelectField, FormSwitchField } =
|
||||
useFormFields<EnquiryFormValues>();
|
||||
const {
|
||||
FormTextField,
|
||||
FormTextareaField,
|
||||
FormSelectField,
|
||||
FormSwitchField,
|
||||
FormDatePickerField,
|
||||
FormCurrencyField,
|
||||
FormPercentageField
|
||||
} = useFormFields<EnquiryFormValues>();
|
||||
const projectPartiesQuery = useQuery({
|
||||
...enquiryProjectPartiesOptions(enquiry?.id ?? ''),
|
||||
enabled: open && !!enquiry?.id
|
||||
@@ -354,22 +361,26 @@ export function EnquiryFormSheet({
|
||||
label: item.label
|
||||
}))}
|
||||
/>
|
||||
<FormTextField
|
||||
<FormCurrencyField
|
||||
name='estimatedValue'
|
||||
label='มูลค่าประมาณการ'
|
||||
type='number'
|
||||
currencyLabel='THB'
|
||||
placeholder='2500000'
|
||||
/>
|
||||
<FormTextField name='chancePercent' label='โอกาสปิดการขาย %' type='number' placeholder='60' />
|
||||
<FormTextField
|
||||
<FormPercentageField
|
||||
name='chancePercent'
|
||||
label='โอกาสปิดการขาย %'
|
||||
placeholder='60'
|
||||
/>
|
||||
<FormDatePickerField
|
||||
name='expectedCloseDate'
|
||||
label='วันที่คาดว่าจะปิดการขาย'
|
||||
placeholder='YYYY-MM-DD'
|
||||
/>
|
||||
<FormTextField name='competitor' label='คู่แข่ง' placeholder='Other vendor name' />
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='description'
|
||||
size='md'
|
||||
label='รายละเอียด'
|
||||
placeholder='สรุปภาพรวมของลีดหรือโอกาสขาย'
|
||||
/>
|
||||
@@ -377,6 +388,7 @@ export function EnquiryFormSheet({
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='requirement'
|
||||
size='md'
|
||||
label='ความต้องการ'
|
||||
placeholder='ความต้องการเชิงเทคนิคหรือเชิงพาณิชย์'
|
||||
/>
|
||||
@@ -384,6 +396,7 @@ export function EnquiryFormSheet({
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='notes'
|
||||
size='md'
|
||||
label='หมายเหตุ'
|
||||
placeholder='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน'
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
CrmCurrencyInput,
|
||||
CrmDateInput,
|
||||
CrmTextarea
|
||||
} from '@/features/crm/components/crm-form-controls';
|
||||
import {
|
||||
invalidateEnquiryMutationQueries,
|
||||
markEnquiryLostMutation,
|
||||
@@ -323,12 +327,16 @@ export function EnquiryOutcomeCard({
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<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 className='grid gap-4 md:grid-cols-2'>
|
||||
<div className='space-y-2'>
|
||||
<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 className='space-y-2'>
|
||||
<Label>PO Currency</Label>
|
||||
@@ -346,7 +354,12 @@ export function EnquiryOutcomeCard({
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<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>
|
||||
<DialogFooter>
|
||||
@@ -406,7 +419,12 @@ export function EnquiryOutcomeCard({
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<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>
|
||||
<DialogFooter>
|
||||
@@ -441,10 +459,11 @@ export function EnquiryOutcomeCard({
|
||||
</DialogHeader>
|
||||
<div className='space-y-2 py-2'>
|
||||
<Label htmlFor='reopen-reason'>Reopen Reason</Label>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
id='reopen-reason'
|
||||
value={reopenReason}
|
||||
onChange={(e) => setReopenReason(e.target.value)}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -58,7 +58,8 @@ export function FollowupFormSheet({
|
||||
}) {
|
||||
const isEdit = !!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 createMutation = useMutation({
|
||||
@@ -136,11 +137,10 @@ export function FollowupFormSheet({
|
||||
<div className='flex-1 overflow-auto'>
|
||||
<form.AppForm>
|
||||
<form.Form id='enquiry-followup-form' className='grid gap-4 md:grid-cols-2'>
|
||||
<FormTextField
|
||||
<FormDatePickerField
|
||||
name='followupDate'
|
||||
label='Follow-up Date'
|
||||
required
|
||||
placeholder='YYYY-MM-DD'
|
||||
/>
|
||||
<form.AppField
|
||||
name='followupType'
|
||||
@@ -204,10 +204,9 @@ export function FollowupFormSheet({
|
||||
</field.FieldSet>
|
||||
)}
|
||||
/>
|
||||
<FormTextField
|
||||
<FormDatePickerField
|
||||
name='nextFollowupDate'
|
||||
label='Next Follow-up Date'
|
||||
placeholder='YYYY-MM-DD'
|
||||
/>
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextField
|
||||
@@ -219,6 +218,7 @@ export function FollowupFormSheet({
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='outcome'
|
||||
size='md'
|
||||
label='Outcome'
|
||||
placeholder='Customer asked for revised dimensions and a site survey'
|
||||
/>
|
||||
@@ -226,6 +226,7 @@ export function FollowupFormSheet({
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='notes'
|
||||
size='md'
|
||||
label='Notes'
|
||||
placeholder='Internal notes from the follow-up session'
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toast } from 'sonner';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 {
|
||||
approvalByIdOptions,
|
||||
approvalsQueryOptions
|
||||
@@ -80,11 +80,11 @@ export function QuotationApprovalTab({
|
||||
{canSubmitNow ? (
|
||||
<div className='space-y-3 rounded-lg border p-4'>
|
||||
<div className='text-sm font-medium'>Submit quotation into approval flow</div>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={remark}
|
||||
onChange={(event) => setRemark(event.target.value)}
|
||||
placeholder='Optional remark for approvers'
|
||||
rows={3}
|
||||
size='md'
|
||||
/>
|
||||
<Button
|
||||
isLoading={submitApproval.isPending}
|
||||
|
||||
@@ -33,7 +33,13 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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 {
|
||||
createQuotationAttachmentMutation,
|
||||
createQuotationCustomerMutation,
|
||||
@@ -168,12 +174,11 @@ function ItemDialog({
|
||||
placeholder="Description"
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Input
|
||||
<CrmNumberInput
|
||||
value={values.quantity}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, quantity: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setValues((s) => ({ ...s, quantity: value }))
|
||||
}
|
||||
type="number"
|
||||
placeholder="Quantity"
|
||||
/>
|
||||
<Select
|
||||
@@ -199,21 +204,21 @@ function ItemDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Input
|
||||
<CrmCurrencyInput
|
||||
value={values.unitPrice}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, unitPrice: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setValues((s) => ({ ...s, unitPrice: value }))
|
||||
}
|
||||
type="number"
|
||||
placeholder="Unit price"
|
||||
currencyLabel="THB"
|
||||
/>
|
||||
<Input
|
||||
<CrmCurrencyInput
|
||||
value={values.discount}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, discount: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setValues((s) => ({ ...s, discount: value }))
|
||||
}
|
||||
type="number"
|
||||
placeholder="Discount"
|
||||
currencyLabel="THB"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -238,21 +243,21 @@ function ItemDialog({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
<CrmPercentageInput
|
||||
value={values.taxRate}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, taxRate: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setValues((s) => ({ ...s, taxRate: value }))
|
||||
}
|
||||
type="number"
|
||||
placeholder="Tax rate %"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={values.notes}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, notes: e.target.value }))
|
||||
}
|
||||
placeholder="Notes"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -418,11 +423,11 @@ function TopicDialog({
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Topic title"
|
||||
/>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={itemsText}
|
||||
onChange={(e) => setItemsText(e.target.value)}
|
||||
placeholder="One line per topic item"
|
||||
rows={6}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -500,12 +505,12 @@ function FollowupDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Input
|
||||
type="date"
|
||||
<CrmDateInput
|
||||
value={values.followupDate}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, followupDate: e.target.value }))
|
||||
setValues((s) => ({ ...s, followupDate: e }))
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<Select
|
||||
value={values.followupType}
|
||||
@@ -552,12 +557,12 @@ function FollowupDialog({
|
||||
}
|
||||
placeholder="Outcome"
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
<CrmDateInput
|
||||
value={values.nextFollowupDate}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, nextFollowupDate: e.target.value }))
|
||||
setValues((s) => ({ ...s, nextFollowupDate: e }))
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<Input
|
||||
value={values.nextAction}
|
||||
@@ -566,12 +571,13 @@ function FollowupDialog({
|
||||
}
|
||||
placeholder="Next action"
|
||||
/>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={values.notes}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, notes: e.target.value }))
|
||||
}
|
||||
placeholder="Notes"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -668,21 +674,21 @@ function AttachmentDialog({
|
||||
}
|
||||
placeholder="application/pdf"
|
||||
/>
|
||||
<Input
|
||||
<CrmNumberInput
|
||||
value={values.fileSize}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, fileSize: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setValues((s) => ({ ...s, fileSize: value }))
|
||||
}
|
||||
type="number"
|
||||
placeholder="File size bytes"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
<CrmTextarea
|
||||
value={values.description}
|
||||
onChange={(e) =>
|
||||
setValues((s) => ({ ...s, description: e.target.value }))
|
||||
}
|
||||
placeholder="Description"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -10,6 +10,13 @@ import {
|
||||
ProjectPartiesEditor,
|
||||
type ProjectPartyEditorItem
|
||||
} 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 {
|
||||
Select,
|
||||
@@ -27,7 +34,6 @@ import {
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { quotationCustomersOptions } from '../api/queries';
|
||||
import { createQuotationMutation, updateQuotationMutation } from '../api/mutations';
|
||||
import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types';
|
||||
@@ -99,16 +105,21 @@ function toFormState(
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required = false,
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<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}
|
||||
</div>
|
||||
);
|
||||
@@ -308,7 +319,7 @@ export function QuotationFormSheet({
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label='Quotation Type'>
|
||||
<Field label='Quotation Type' required>
|
||||
<Select value={state.quotationType} onValueChange={(value) => setField('quotationType', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select type' />
|
||||
@@ -323,12 +334,12 @@ export function QuotationFormSheet({
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label='Quotation Date'>
|
||||
<Input type='date' value={state.quotationDate} onChange={(e) => setField('quotationDate', e.target.value)} />
|
||||
<Field label='Quotation Date' required>
|
||||
<CrmDateInput value={state.quotationDate} onChange={(value) => setField('quotationDate', value)} />
|
||||
</Field>
|
||||
|
||||
<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 label='Branch'>
|
||||
@@ -378,7 +389,7 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<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 label='Project Name'>
|
||||
@@ -414,11 +425,11 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<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 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 label='Discount Type'>
|
||||
@@ -438,7 +449,7 @@ export function QuotationFormSheet({
|
||||
</Field>
|
||||
|
||||
<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 label='Sent Via'>
|
||||
@@ -472,13 +483,13 @@ export function QuotationFormSheet({
|
||||
|
||||
<div className='md:col-span-2'>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className='md:col-span-2'>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Link from 'next/link';
|
||||
import { parseAsString, useQueryStates } from 'nuqs';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { CrmDateInput } from '@/features/crm/components/crm-form-controls';
|
||||
import type { CrmReportFilterMetadata } from '../api/types';
|
||||
|
||||
type FilterKey =
|
||||
@@ -92,17 +92,15 @@ export function ReportFilterBar({
|
||||
|
||||
<div className='grid gap-3 md:grid-cols-2 xl:grid-cols-6'>
|
||||
{filterKeys.includes('dateFrom') ? (
|
||||
<Input
|
||||
type='date'
|
||||
<CrmDateInput
|
||||
value={params.dateFrom ?? ''}
|
||||
onChange={(event) => void setParams({ dateFrom: event.target.value || null })}
|
||||
onChange={(value) => void setParams({ dateFrom: value || null })}
|
||||
/>
|
||||
) : null}
|
||||
{filterKeys.includes('dateTo') ? (
|
||||
<Input
|
||||
type='date'
|
||||
<CrmDateInput
|
||||
value={params.dateTo ?? ''}
|
||||
onChange={(event) => void setParams({ dateTo: event.target.value || null })}
|
||||
onChange={(value) => void setParams({ dateTo: value || 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