From 6d6cd3a6df5e381bbe40a6e65d30b174dc986944 Mon Sep 17 00:00:00 2001 From: phaichayon Date: Wed, 1 Jul 2026 08:44:35 +0700 Subject: [PATCH] d.6 --- .../pdf-audit/payload-parity-report.json | 8 +- .../pdf-audit/runtime-payload-report.json | 134 ++-- .../pdf-audit/signature-audit-report.json | 8 +- artifacts/pdf-audit/summary.json | 4 +- artifacts/pdf-audit/summary.md | 4 +- .../pdf-audit/template-version-report.json | 2 +- artifacts/pdf-audit/topic-runtime-report.json | 2 +- docs/implementation/pdf-audit-report.json | 6 +- docs/implementation/pdf-audit-report.md | 4 +- ...ndation-audit-log-separation-2026-07-01.md | 38 ++ next-env.d.ts | 2 +- plans/task-d.6.1.md | 127 ++++ plans/task-d.6.md | 642 ++++++++++++++++++ .../forms/fields/date-picker-field.tsx | 2 +- src/features/crm/activity/types.ts | 86 +++ src/features/crm/audit-log/types.ts | 12 + src/features/crm/components/audit-log-tab.tsx | 119 ++++ .../crm/components/crm-form-controls.tsx | 59 +- src/features/crm/customers/api/types.ts | 11 +- .../customers/components/customer-detail.tsx | 43 +- src/features/crm/customers/server/service.ts | 3 + .../crm/leads/components/lead-detail.tsx | 28 +- .../leads/components/lead-followup-panel.tsx | 130 ++-- src/features/crm/leads/server/service.ts | 6 +- src/features/crm/leads/types.ts | 11 +- src/features/crm/opportunities/api/types.ts | 11 +- .../components/opportunity-detail.tsx | 28 +- .../crm/opportunities/server/service.ts | 5 +- src/features/crm/quotations/api/types.ts | 11 +- .../components/quotation-detail.tsx | 48 +- src/features/crm/quotations/server/service.ts | 5 +- src/lib/auth/rbac.ts | 18 + 32 files changed, 1284 insertions(+), 333 deletions(-) create mode 100644 docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md create mode 100644 plans/task-d.6.1.md create mode 100644 plans/task-d.6.md create mode 100644 src/features/crm/activity/types.ts create mode 100644 src/features/crm/audit-log/types.ts create mode 100644 src/features/crm/components/audit-log-tab.tsx diff --git a/artifacts/pdf-audit/payload-parity-report.json b/artifacts/pdf-audit/payload-parity-report.json index 02db715..944007a 100644 --- a/artifacts/pdf-audit/payload-parity-report.json +++ b/artifacts/pdf-audit/payload-parity-report.json @@ -1,10 +1,10 @@ { "audit": "payload-parity", "overallStatus": "PASS", - "quotationCode": "CRA2606-1108", - "previewHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", - "generationHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", - "snapshotHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", + "quotationCode": "CRA2606-1107", + "previewHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6", + "generationHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6", + "snapshotHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6", "mismatches": [], "comparableFieldCount": 31, "topicInputKeyCount": 6, diff --git a/artifacts/pdf-audit/runtime-payload-report.json b/artifacts/pdf-audit/runtime-payload-report.json index 9f62c83..d829a65 100644 --- a/artifacts/pdf-audit/runtime-payload-report.json +++ b/artifacts/pdf-audit/runtime-payload-report.json @@ -2,7 +2,7 @@ "audit": "runtime-payload", "overallStatus": "PASS", "staticLabelsStatus": "PASS", - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "templateName": "ALLA Demo Quotation Standard", "templateVersion": "1.0", "findingCount": 168, @@ -25,28 +25,28 @@ "field": "documentData.customer.id", "source": "documentData", "status": "PASS", - "value": "cf2c1f29-ce0a-41dd-acc0-13b7f82086cd", + "value": "a476b13f-87f5-47dc-9606-e88ec24cda87", "message": "String is populated" }, { "field": "documentData.customer.name", "source": "documentData", "status": "PASS", - "value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ", + "value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด", "message": "String is populated" }, { "field": "documentData.customer.address", "source": "documentData", "status": "PASS", - "value": "Bang Sue, Bangkok", + "value": "Mueang Chiang Mai, Chiang Mai", "message": "String is populated" }, { "field": "documentData.customer.phone", "source": "documentData", "status": "PASS", - "value": "02-400827", + "value": "02-400824", "message": "String is populated" }, { @@ -60,14 +60,14 @@ "field": "documentData.contact.id", "source": "documentData", "status": "PASS", - "value": "7c88a108-419f-47ed-a62b-1d784a7b8455", + "value": "78c92957-126f-4765-873f-14ce01330be9", "message": "String is populated" }, { "field": "documentData.contact.name", "source": "documentData", "status": "PASS", - "value": "คุณวราภรณ์ เกษมศรี", + "value": "คุณกิตติพงศ์ สุวรรณ", "message": "String is populated" }, { @@ -81,56 +81,56 @@ "field": "documentData.contact.mobile", "source": "documentData", "status": "PASS", - "value": "0810000250", + "value": "0810000220", "message": "String is populated" }, { "field": "documentData.contact.position", "source": "documentData", "status": "PASS", - "value": "Procurement Manager", + "value": "Plant Manager", "message": "String is populated" }, { "field": "documentData.quotation.id", "source": "documentData", "status": "PASS", - "value": "0bbb3fce-f3a2-408a-b93a-6faaa4d74f55", + "value": "c0a3c490-30ae-4c77-8f80-ec9328eeb66e", "message": "String is populated" }, { "field": "documentData.quotation.code", "source": "documentData", "status": "PASS", - "value": "CRA2606-1108", + "value": "CRA2606-1107", "message": "String is populated" }, { "field": "documentData.quotation.projectName", "source": "documentData", "status": "PASS", - "value": "Material Handling Lab Crane", + "value": "Dealer Stock Crane Package", "message": "String is populated" }, { "field": "documentData.quotation.projectLocation", "source": "documentData", "status": "PASS", - "value": "Bang Sue Engineering Faculty", + "value": "Chiang Mai Service Warehouse", "message": "String is populated" }, { "field": "documentData.quotation.attention", "source": "documentData", "status": "PASS", - "value": "คุณวราภรณ์ เกษมศรี", + "value": "คุณกิตติพงศ์ สุวรรณ", "message": "String is populated" }, { "field": "documentData.quotation.reference", "source": "documentData", "status": "PASS", - "value": "crm-uat-seed-v1:university-lab-crane-approved", + "value": "crm-uat-seed-v1:dealer-channel-approved", "message": "String is populated" }, { @@ -151,14 +151,14 @@ "field": "documentData.quotation.subtotal", "source": "documentData", "status": "PASS", - "value": 3008000, + "value": 4418000, "message": "Scalar value is populated" }, { "field": "documentData.quotation.totalAmount", "source": "documentData", "status": "PASS", - "value": 3122003, + "value": 4585442, "message": "Scalar value is populated" }, { @@ -172,7 +172,7 @@ "field": "documentData.items[0].id", "source": "documentData", "status": "PASS", - "value": "f37f7d99-f1e8-4aa9-a207-0a280ec74e26", + "value": "7ffd6a54-7c9f-4ab2-b6b2-7a38a0f680e9", "message": "String is populated" }, { @@ -186,7 +186,7 @@ "field": "documentData.items[0].description", "source": "documentData", "status": "PASS", - "value": "Main equipment supply for Material Handling Lab Crane", + "value": "Main equipment supply for Dealer Stock Crane Package", "message": "String is populated" }, { @@ -207,7 +207,7 @@ "field": "documentData.items[0].unitPrice", "source": "documentData", "status": "PASS", - "value": 1717102, + "value": 2521993, "message": "Scalar value is populated" }, { @@ -221,14 +221,14 @@ "field": "documentData.items[0].totalPrice", "source": "documentData", "status": "PASS", - "value": 1717102, + "value": 2521993, "message": "Scalar value is populated" }, { "field": "documentData.items[1].id", "source": "documentData", "status": "PASS", - "value": "63809391-7dd6-4ebd-8aba-3cb5d18b148a", + "value": "02c15bda-11ae-4e5c-a0f4-a5943214abec", "message": "String is populated" }, { @@ -242,7 +242,7 @@ "field": "documentData.items[1].description", "source": "documentData", "status": "PASS", - "value": "Installation and commissioning for Material Handling Lab Crane", + "value": "Installation and commissioning for Dealer Stock Crane Package", "message": "String is populated" }, { @@ -263,7 +263,7 @@ "field": "documentData.items[1].unitPrice", "source": "documentData", "status": "PASS", - "value": 874161, + "value": 1283924, "message": "Scalar value is populated" }, { @@ -277,14 +277,14 @@ "field": "documentData.items[1].totalPrice", "source": "documentData", "status": "PASS", - "value": 874161, + "value": 1283924, "message": "Scalar value is populated" }, { "field": "documentData.items[2].id", "source": "documentData", "status": "PASS", - "value": "6e304b78-bbb6-4beb-b04e-6958a9e3ca69", + "value": "d13a4d65-7b45-46d5-a2d1-2f79db82779b", "message": "String is populated" }, { @@ -298,7 +298,7 @@ "field": "documentData.items[2].description", "source": "documentData", "status": "PASS", - "value": "Operator training and documentation for Material Handling Lab Crane", + "value": "Operator training and documentation for Dealer Stock Crane Package", "message": "String is populated" }, { @@ -319,7 +319,7 @@ "field": "documentData.items[2].unitPrice", "source": "documentData", "status": "PASS", - "value": 312200, + "value": 458544, "message": "Scalar value is populated" }, { @@ -333,14 +333,14 @@ "field": "documentData.items[2].totalPrice", "source": "documentData", "status": "PASS", - "value": 312200, + "value": 458544, "message": "Scalar value is populated" }, { "field": "documentData.items[3].id", "source": "documentData", "status": "PASS", - "value": "e28c0f75-086f-4d4a-bc81-a3292fce97b2", + "value": "970d78d6-939f-4856-a17f-3538983f149a", "message": "String is populated" }, { @@ -354,7 +354,7 @@ "field": "documentData.items[3].description", "source": "documentData", "status": "PASS", - "value": "Project management and testing for Material Handling Lab Crane", + "value": "Project management and testing for Dealer Stock Crane Package", "message": "String is populated" }, { @@ -375,7 +375,7 @@ "field": "documentData.items[3].unitPrice", "source": "documentData", "status": "PASS", - "value": 218540, + "value": 320981, "message": "Scalar value is populated" }, { @@ -389,7 +389,7 @@ "field": "documentData.items[3].totalPrice", "source": "documentData", "status": "PASS", - "value": 218540, + "value": 320981, "message": "Scalar value is populated" }, { @@ -403,14 +403,14 @@ "field": "documentData.quotationCustomers[0].id", "source": "documentData", "status": "PASS", - "value": "3eb300e9-8410-4a97-b2f3-2fb935e60a0b", + "value": "40ef3613-a862-4166-be00-b79237e80a68", "message": "String is populated" }, { "field": "documentData.quotationCustomers[0].customerId", "source": "documentData", "status": "PASS", - "value": "cf2c1f29-ce0a-41dd-acc0-13b7f82086cd", + "value": "a476b13f-87f5-47dc-9606-e88ec24cda87", "message": "String is populated" }, { @@ -438,7 +438,7 @@ "field": "documentData.topics.all[0].id", "source": "documentData", "status": "PASS", - "value": "3013a775-bc86-4b22-90f9-048fa1225a91", + "value": "7fdc1ec7-fd3b-479c-8688-913c4e54329b", "message": "String is populated" }, { @@ -473,7 +473,7 @@ "field": "documentData.topics.all[0].items[0].content", "source": "documentData", "status": "PASS", - "value": "Supply and execute material handling lab crane according to approved drawing.", + "value": "Supply and execute dealer stock crane package according to approved drawing.", "message": "String is populated" }, { @@ -501,7 +501,7 @@ "field": "documentData.topics.all[1].id", "source": "documentData", "status": "PASS", - "value": "bf614822-2e02-456e-af35-c9dc50b0140f", + "value": "921523e3-c2a5-4be1-8d10-a2a4c2913176", "message": "String is populated" }, { @@ -564,7 +564,7 @@ "field": "documentData.topics.all[2].id", "source": "documentData", "status": "PASS", - "value": "eee9e6f9-15d5-4540-9cce-628026bc0281", + "value": "b3138a62-36c3-4ffb-9504-08479aa3d263", "message": "String is populated" }, { @@ -634,7 +634,7 @@ "field": "documentData.topics.exclusion[0].id", "source": "documentData", "status": "PASS", - "value": "bf614822-2e02-456e-af35-c9dc50b0140f", + "value": "921523e3-c2a5-4be1-8d10-a2a4c2913176", "message": "String is populated" }, { @@ -732,14 +732,14 @@ "field": "documentData.pdfme.quotation_date", "source": "documentData", "status": "PASS", - "value": "Jun 15, 2026", + "value": "Jun 3, 2026", "message": "String is populated" }, { "field": "documentData.pdfme.quotation_price", "source": "documentData", "status": "PASS", - "value": "THB 3,008,000.00", + "value": "THB 4,418,000.00", "message": "String is populated" }, { @@ -816,70 +816,70 @@ "field": "documentData.signatures.preparedBy.actedAt", "source": "documentData", "status": "PASS", - "value": "2026-06-15T09:00:00.000Z", + "value": "2026-06-03T09:00:00.000Z", "message": "String is populated" }, { "field": "documentData.signatures.approvedBy.name", "source": "documentData", "status": "PASS", - "value": "-", + "value": "Sales Manager UAT", "message": "String is populated" }, { "field": "documentData.signatures.approvedBy.position", "source": "documentData", "status": "PASS", - "value": "-", + "value": "Sales Manager", "message": "String is populated" }, { "field": "documentData.signatures.approvedBy.actedAt", "source": "documentData", - "status": "WARNING", - "value": null, - "message": "Value is null" + "status": "PASS", + "value": "2026-06-06T10:00:00.000Z", + "message": "String is populated" }, { "field": "documentData.signatures.authorizedBy.name", "source": "documentData", "status": "PASS", - "value": "Sales Manager UAT", + "value": "CEO UAT", "message": "String is populated" }, { "field": "documentData.signatures.authorizedBy.position", "source": "documentData", "status": "PASS", - "value": "Sales Manager", + "value": "General Manager", "message": "String is populated" }, { "field": "documentData.signatures.authorizedBy.actedAt", "source": "documentData", "status": "PASS", - "value": "2026-06-17T11:00:00.000Z", + "value": "2026-06-07T09:00:00.000Z", "message": "String is populated" }, { "field": "templateInput.customer_name", "source": "templateInput", "status": "PASS", - "value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ", + "value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด", "message": "String is populated" }, { "field": "templateInput.customer_addr", "source": "templateInput", "status": "PASS", - "value": "Bang Sue, Bangkok", + "value": "Mueang Chiang Mai, Chiang Mai", "message": "String is populated" }, { "field": "templateInput.customer_tel", "source": "templateInput", "status": "PASS", - "value": "02-400827", + "value": "02-400824", "message": "String is populated" }, { @@ -893,35 +893,35 @@ "field": "templateInput.customer_att", "source": "templateInput", "status": "PASS", - "value": "คุณวราภรณ์ เกษมศรี", + "value": "คุณกิตติพงศ์ สุวรรณ", "message": "String is populated" }, { "field": "templateInput.project_name", "source": "templateInput", "status": "PASS", - "value": "Material Handling Lab Crane", + "value": "Dealer Stock Crane Package", "message": "String is populated" }, { "field": "templateInput.site_location", "source": "templateInput", "status": "PASS", - "value": "Bang Sue Engineering Faculty", + "value": "Chiang Mai Service Warehouse", "message": "String is populated" }, { "field": "templateInput.quotation_date_data", "source": "templateInput", "status": "PASS", - "value": "Jun 15, 2026", + "value": "Jun 3, 2026", "message": "String is populated" }, { "field": "templateInput.quotation_code_data", "source": "templateInput", "status": "PASS", - "value": "CRA2606-1108", + "value": "CRA2606-1107", "message": "String is populated" }, { @@ -935,7 +935,7 @@ "field": "templateInput.quotation_price", "source": "templateInput", "status": "PASS", - "value": "THB 3,008,000.00", + "value": "THB 4,418,000.00", "message": "String is populated" }, { @@ -998,35 +998,35 @@ "field": "templateInput.app2", "source": "templateInput", "status": "PASS", - "value": "-", + "value": "Sales Manager UAT", "message": "String is populated" }, { "field": "templateInput.app2_position", "source": "templateInput", "status": "PASS", - "value": "-", + "value": "Sales Manager", "message": "String is populated" }, { "field": "templateInput.app3", "source": "templateInput", "status": "PASS", - "value": "Sales Manager UAT", + "value": "CEO UAT", "message": "String is populated" }, { "field": "templateInput.app3_position", "source": "templateInput", "status": "PASS", - "value": "Sales Manager", + "value": "General Manager", "message": "String is populated" }, { "field": "templateInput.quotation_date", "source": "templateInput", "status": "PASS", - "value": "Jun 15, 2026", + "value": "Jun 3, 2026", "message": "String is populated" }, { @@ -1075,7 +1075,7 @@ "field": "templateInput.quotation_code", "source": "templateInput", "status": "PASS", - "value": "CRA2606-1108", + "value": "CRA2606-1107", "message": "String is populated" }, { @@ -1138,7 +1138,7 @@ "field": "templateInput.quotation_price_data[0][1]", "source": "templateInput", "status": "PASS", - "value": "฿3,008,000.00", + "value": "฿4,418,000.00", "message": "Price table amount cell is formatted" }, { diff --git a/artifacts/pdf-audit/signature-audit-report.json b/artifacts/pdf-audit/signature-audit-report.json index 2cb365c..4368e34 100644 --- a/artifacts/pdf-audit/signature-audit-report.json +++ b/artifacts/pdf-audit/signature-audit-report.json @@ -1,14 +1,14 @@ { "audit": "approved-snapshot-parity", "overallStatus": "PASS", - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "approvedSnapshotAvailable": true, "approvedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84", "computedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84", "inconsistentTemplateKeys": [], - "computedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229", - "storedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229", - "approvedArtifactReference": null, + "computedSignatureHash": "4106a8d9883d207231103e13f29aff692e6ad1d81a0e61cb26cea7ceea73e3b8", + "storedSignatureHash": "4106a8d9883d207231103e13f29aff692e6ad1d81a0e61cb26cea7ceea73e3b8", + "approvedArtifactReference": "artifact:7088f4e9-c56a-4389-975b-3c7ccf13c772", "refreshedFixtureSnapshot": true, "usedAuditFixture": false } diff --git a/artifacts/pdf-audit/summary.json b/artifacts/pdf-audit/summary.json index 07d177a..88af5d6 100644 --- a/artifacts/pdf-audit/summary.json +++ b/artifacts/pdf-audit/summary.json @@ -1,7 +1,7 @@ { - "generatedAt": "2026-06-30T16:18:58.393Z", + "generatedAt": "2026-06-30T23:54:34.472Z", "overallStatus": "PASS", - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "artifacts": { "templateVersionReport": "template-version-report.json", "placeholderIntegrityReport": "placeholder-integrity-report.json", diff --git a/artifacts/pdf-audit/summary.md b/artifacts/pdf-audit/summary.md index f786102..098101b 100644 --- a/artifacts/pdf-audit/summary.md +++ b/artifacts/pdf-audit/summary.md @@ -1,8 +1,8 @@ # PDF Integrity Summary -- Generated At: 2026-06-30T16:18:58.393Z +- Generated At: 2026-06-30T23:54:34.472Z - Overall Status: PASS -- Quotation Code: CRA2606-1108 +- Quotation Code: CRA2606-1107 ## Statuses diff --git a/artifacts/pdf-audit/template-version-report.json b/artifacts/pdf-audit/template-version-report.json index e460b67..0be0b51 100644 --- a/artifacts/pdf-audit/template-version-report.json +++ b/artifacts/pdf-audit/template-version-report.json @@ -2,7 +2,7 @@ "audit": "template-version-integrity", "overallStatus": "PASS", "activeTemplateCount": 1, - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "approvedTemplateVersionId": null, "approvedTemplateVersionIsActive": false, "issues": [] diff --git a/artifacts/pdf-audit/topic-runtime-report.json b/artifacts/pdf-audit/topic-runtime-report.json index fd65a3d..ca4af66 100644 --- a/artifacts/pdf-audit/topic-runtime-report.json +++ b/artifacts/pdf-audit/topic-runtime-report.json @@ -1,7 +1,7 @@ { "audit": "topic-runtime", "overallStatus": "PASS", - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "topicCount": 3, "topicInputKeys": [ "topic_1_0", diff --git a/docs/implementation/pdf-audit-report.json b/docs/implementation/pdf-audit-report.json index b124f9a..9491ec0 100644 --- a/docs/implementation/pdf-audit-report.json +++ b/docs/implementation/pdf-audit-report.json @@ -1,7 +1,7 @@ { - "generatedAt": "2026-06-30T16:18:57.428Z", + "generatedAt": "2026-06-30T23:54:26.582Z", "overallStatus": "PASS", - "quotationCode": "CRA2606-1108", + "quotationCode": "CRA2606-1107", "expectedFixtureCode": "QT-H5-AUDIT", "template": { "templateName": "ALLA Demo Quotation Standard", @@ -263,7 +263,7 @@ "consistency": { "status": "PASS", "approvedSnapshotAvailable": true, - "approvedArtifactReference": null, + "approvedArtifactReference": "artifact:7088f4e9-c56a-4389-975b-3c7ccf13c772", "inconsistentKeys": [] }, "integritySummary": { diff --git a/docs/implementation/pdf-audit-report.md b/docs/implementation/pdf-audit-report.md index e61b192..c996a77 100644 --- a/docs/implementation/pdf-audit-report.md +++ b/docs/implementation/pdf-audit-report.md @@ -1,8 +1,8 @@ # PDF Audit Report -- Generated At: 2026-06-30T16:18:57.428Z +- Generated At: 2026-06-30T23:54:26.582Z - Overall Status: PASS -- Quotation Code: CRA2606-1108 +- Quotation Code: CRA2606-1107 - Template: ALLA Demo Quotation Standard v1.0 - Previous Version: None diff --git a/docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md b/docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md new file mode 100644 index 0000000..24c78bf --- /dev/null +++ b/docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md @@ -0,0 +1,38 @@ +# Task D.6 Implementation Report - 2026-07-01 + +## Scope +- Separate CRM business `Activity` semantics from system-generated audit history. +- Rename CRM detail tabs that currently show system history from `Activity` to `Audit Log`. +- Improve follow-up date input UX with the shared calendar picker and stable `YYYY-MM-DD` display. +- Introduce a contract-first CRM Activity foundation without migrating existing follow-up storage in this phase. + +## Review Summary +Reviewed before implementation: +- [AGENTS.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/AGENTS.md) +- [plans/task-d.6.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-d.6.md) +- [docs/standards/project-foundations.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/project-foundations.md) +- [docs/standards/architecture-rules.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/architecture-rules.md) +- [docs/standards/ui-ux-rules.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/ui-ux-rules.md) +- [docs/standards/task-review-checklist.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/task-review-checklist.md) +- Related CRM detail, follow-up, and audit-log foundations under `src/features/crm/**` and `src/features/foundation/audit-log/**` + +## Changes +- Added shared [audit-log-tab.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/components/audit-log-tab.tsx) and [types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/audit-log/types.ts) for consistent CRM audit history rendering. +- Updated lead, opportunity, customer, and quotation detail views to label system history as `Audit Log`. +- Enriched CRM audit-log records in lead, opportunity, customer, and quotation services so UI can consume `beforeData`, `afterData`, and `requestId`. +- Standardized [crm-form-controls.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/components/crm-form-controls.tsx) and [date-picker-field.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/components/forms/fields/date-picker-field.tsx) to show `YYYY-MM-DD` and allow clearing optional dates. +- Rebuilt [lead-followup-panel.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/leads/components/lead-followup-panel.tsx) to use the shared date-picker field instead of plain text date entry. +- Added [src/features/crm/activity/types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/activity/types.ts) to define future activity entity scope, types, statuses, priorities, permission contracts, and overdue-status derivation. +- Added activity and audit-log permission keys in [rbac.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts). + +## Verification +- `npm run typecheck` -> PASS +- `npm run build` -> PASS +- `npm run audit:pdf` -> PASS + +## Known Follow-up +- Historical lead/opportunity/quotation follow-up rows still live in their existing tables and APIs; only the UX and activity contracts were prepared in this phase. +- `npm run build` still emits pre-existing Turbopack warnings about workspace root lockfile detection and NFT tracing around PDF audit/template validation paths. No new blocking build issue was introduced by D.6. + +## Outcome +Task D.6 now reserves `Activity` for future MK/Sales business work, presents current system history as `Audit Log`, improves lead follow-up date entry with a proper calendar picker, and establishes the first reusable contracts for a future CRM activity module. diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/plans/task-d.6.1.md b/plans/task-d.6.1.md new file mode 100644 index 0000000..118d1f7 --- /dev/null +++ b/plans/task-d.6.1.md @@ -0,0 +1,127 @@ +# Task D.6.1 – Lead Create Form UX & Default Rules + +## Objective + +ปรับหน้า Create Lead ให้เหมาะกับการสร้าง Lead ใหม่จริง โดยซ่อน field ที่ยังไม่ควรใช้ตอนเริ่มต้น และตั้งค่า default ตาม business rule + +--- + +## Requirements + +### 1. Hide fields on Create Lead + +ตอนสร้าง Lead ใหม่ ไม่ต้องแสดง field ต่อไปนี้: + +* Lost Reason +* Status +* Marketing Owner + +Field เหล่านี้อาจแสดงในหน้า Edit/Detail ตาม permission และสถานะภายหลังได้ + +--- + +### 2. Default Lead Status + +เมื่อสร้าง Lead ใหม่ ให้ระบบตั้งสถานะอัตโนมัติเป็น: + +```text +new_job +``` + +หรือ label: + +```text +New Job +``` + +User ไม่ต้องเลือกเองในหน้า Create + +--- + +### 3. Default Marketing Owner + +Marketing Owner ต้อง default เป็นผู้สร้างเอกสาร + +```text +marketingOwnerId = currentUser.id +``` + +ไม่ต้องให้ user เลือกตอน Create + +--- + +### 4. Add Lead Source field + +เพิ่ม field: + +```text +Lead Source +``` + +ใช้เก็บที่มาของ Lead เช่น: + +* Website +* Facebook +* Line OA +* Phone Call +* Walk-in +* Referral +* Existing Customer +* Exhibition +* Sales Visit +* Other + +ควรใช้ master option / ms_options ถ้ามี foundation รองรับอยู่แล้ว + +--- + +## Create vs Edit Behavior + +### Create Lead + +แสดงเฉพาะ field ที่จำเป็นสำหรับเปิด Lead ใหม่ + +ไม่แสดง: + +* Lost Reason +* Status +* Marketing Owner + +ระบบกำหนดให้อัตโนมัติ + +--- + +### Edit Lead / Detail Lead + +สามารถแสดง field เพิ่มเติมตามสถานะและ permission เช่น: + +* Status +* Lost Reason เมื่อ status เป็น lost/closed lost +* Marketing Owner สำหรับ admin/manager ถ้าต้อง reassign + +--- + +## Backend Rules + +ฝั่ง API/service ต้อง enforce ด้วย ไม่พึ่ง UI อย่างเดียว: + +* ถ้า create lead ไม่มี status → set `new_job` +* ถ้า create lead ไม่มี marketingOwnerId → set current user +* ห้าม user override marketingOwnerId ถ้าไม่มี permission +* Lost Reason ต้องไม่ required ตอน create +* Lead Source ควรถูก validate จาก master option + +--- + +## Acceptance Criteria + +* Create Lead ไม่แสดง Lost Reason +* Create Lead ไม่แสดง Status +* Create Lead ไม่แสดง Marketing Owner +* Lead ใหม่ถูกสร้างด้วย status `New Job` +* Marketing Owner เป็น user ที่สร้าง Lead +* มี field Lead Source +* Lead Source บันทึกได้ถูกต้อง +* Edit/Detail ยังรองรับ status/lost reason ตาม business rule +* `npm run typecheck` +* `npm run build` diff --git a/plans/task-d.6.md b/plans/task-d.6.md new file mode 100644 index 0000000..8f64149 --- /dev/null +++ b/plans/task-d.6.md @@ -0,0 +1,642 @@ +# Task D.6 – CRM Activity Foundation & Audit Log Separation + +## Objective + +Separate system audit history from real CRM business activities. + +Currently, several CRM document detail pages use the tab name `Activity` for system-generated history. This is semantically incorrect. + +In CRM, `Activity` should mean actionable work performed by MK/Sales, such as: + +* site visit +* customer meeting +* phone call +* follow-up +* requirement discussion +* site survey +* quotation follow-up + +System-generated history should be displayed under `Audit Log`. + +This task also improves follow-up date input UX by replacing plain date text input with a calendar date picker. + +--- + +# Background + +Current issue: + +```text id="b2lkfk" +Activity tab +``` + +is being used for system history such as: + +* created +* updated +* status changed +* approved +* rejected +* PDF generated +* customer package generated + +But business users expect Activity to mean: + +```text id="f8lcxc" +2026-07-01: Visit customer site + +2026-07-03: Follow up quotation by phone + +2026-07-05: Meeting with purchasing team +``` + +Therefore: + +* System history = Audit Log +* Business work = Activity + +--- + +# Scope + +Included: + +* Rename system history tab from `Activity` to `Audit Log` +* Standardize Audit Log tab across CRM documents +* Create CRM Activity foundation model +* Prepare Activity UI patterns +* Improve follow-up date input to use calendar picker +* Keep existing follow-up business behavior compatible +* Prepare future Calendar integration + +Excluded: + +* Full calendar module implementation +* Google Calendar / Outlook sync +* Notification automation +* Advanced recurring activity scheduling +* Mobile push reminders + +--- + +# Discovery Requirement + +Before implementation, inspect the actual codebase. + +Review at minimum: + +```text id="7scakw" +Quotation detail tabs +Lead detail tabs +Opportunity detail tabs +Customer detail tabs +Follow-up components +Audit log components +Existing activity/follow-up tables +Existing calendar/date picker components +Existing shadcn calendar/popover/date-input patterns +``` + +Identify: + +* where `Activity` is used as audit history +* where follow-up forms use plain date input +* which modules already have follow-up data +* which audit log API/components can be reused +* current date format standards + +Do not assume the current UI structure. + +--- + +# Terminology Rules + +## Audit Log + +Means system-generated immutable history. + +Examples: + +```text id="zg8qvr" +Quotation Created + +Status Changed + +Approval Requested + +Approved + +Rejected + +PDF Generated + +Customer Package Generated + +Email Sent +``` + +Characteristics: + +* read-only +* system generated +* immutable +* audit/compliance focused + +--- + +## Activity + +Means CRM business work. + +Examples: + +```text id="g7k2rv" +Visit customer site + +Call customer + +Follow-up quotation + +Site survey + +Requirement gathering + +Meeting purchasing team + +Send revised offer +``` + +Characteristics: + +* created by user +* assigned to user +* has due date +* may be planned or completed +* can appear in calendar +* may trigger notification/reminder later + +--- + +# Part 1 - Audit Log Tab Refactor + +Rename all tabs that currently show system history from: + +```text id="q1j1zt" +Activity +``` + +to: + +```text id="bmkkw7" +Audit Log +``` + +Apply at minimum to: + +* Quotation detail +* Lead detail +* Opportunity detail +* Customer detail +* Any CRM document detail page that uses Activity for history + +If a page already has true Activity content, do not rename it blindly. Split it into: + +```text id="mfooo0" +Activity + +Audit Log +``` + +--- + +# Audit Log UI Standard + +Create or reuse a shared component: + +```text id="tk4rn3" + +``` + +Display: + +* timestamp +* actor +* action +* before/after state +* remarks +* related artifact +* system-generated badge if applicable + +Sort: + +```text id="0or6f8" +newest first +``` + +Do not allow editing or deleting audit rows. + +--- + +# Part 2 - CRM Activity Foundation + +Design a reusable CRM Activity model. + +Activities should support multiple CRM entities: + +* Lead +* Opportunity +* Quotation +* Customer +* Contact +* Future PO / Service Report + +Suggested fields: + +```text id="svj0zq" +id +organizationId +entityType +entityId +activityType +subject +description +ownerId +assignedToId +dueDate +dueTime +completedAt +status +priority +outcome +nextAction +createdBy +createdAt +updatedAt +``` + +--- + +# Activity Types + +Initial activity types: + +```text id="zmet5e" +site_visit +meeting +phone_call +email_follow_up +quotation_follow_up +site_survey +requirement_discussion +internal_task +other +``` + +--- + +# Activity Status + +Use: + +```text id="8cmuir" +planned +in_progress +completed +cancelled +overdue +``` + +Overdue may be derived, not necessarily stored. + +--- + +# Activity UI + +For each supported CRM entity, Activity tab should eventually show: + +```text id="bn6gr5" +Upcoming Activities + +Completed Activities + +Overdue Activities +``` + +Each activity card should show: + +* activity type +* subject +* due date +* owner +* status +* priority +* related document +* outcome if completed + +--- + +# Part 3 - Follow-up Date Picker Improvement + +Replace plain follow-up date input with a calendar date picker. + +Apply to existing follow-up forms in: + +* Lead follow-up +* Opportunity follow-up +* Quotation follow-up +* Customer follow-up if present + +Date input must be easy for users to click and choose. + +Use existing UI components if available: + +```text id="s1f84i" +Calendar +Popover +Button +FormControl +``` + +Preferred UX: + +```text id="t9fzwi" +[ Select follow-up date 📅 ] +``` + +On click: + +```text id="far25j" +Calendar opens +User selects date +Date displays as YYYY-MM-DD +``` + +--- + +# Date Format Rule + +Display dates as: + +```text id="srzquj" +YYYY-MM-DD +``` + +Storage should use existing backend-compatible date format. + +Do not introduce locale-dependent rendering that may cause hydration mismatch. + +--- + +# Follow-up UX Rules + +* Calendar picker must support keyboard navigation. +* Date must be clear after selection. +* Existing validation must remain. +* Required date rules must remain. +* User should not need to manually type dates. +* Manual typing may be disabled unless existing component requires it. +* If clearing date is allowed, provide a clear button. + +--- + +# Part 4 - Compatibility With Existing Follow-up + +Do not break existing follow-up APIs. + +If current follow-up is stored separately from the new Activity model, keep it compatible. + +Recommended strategy: + +```text id="xyx67z" +Phase D.6 + +Rename Audit Log tab + +Improve follow-up date picker + +Introduce Activity foundation + +Do not migrate all follow-ups yet +``` + +Future task can migrate: + +```text id="543zj8" +Follow-up + +↓ + +Activity +``` + +--- + +# Part 5 - Calendar Readiness + +Activities must be designed so future calendar integration can use them. + +Required future-ready fields: + +* dueDate +* dueTime +* assignedToId +* ownerId +* status +* entityType +* entityId + +This prepares future integration with: + +```text id="ksqzgi" +Calendar module + +Notifications + +Reminders + +Team visibility +``` + +--- + +# Permissions + +Add or reuse permissions: + +```text id="kujab3" +crmActivityRead +crmActivityCreate +crmActivityUpdate +crmActivityComplete +crmActivityDelete +crmAuditLogRead +``` + +Audit log should be read-only. + +Activity should follow CRM visibility rules. + +--- + +# Audit Logging + +Creating, updating, completing, or cancelling an Activity should write audit logs. + +Examples: + +```text id="kqf2c1" +Activity Created + +Activity Completed + +Activity Cancelled + +Activity Rescheduled +``` + +--- + +# Testing Requirements + +Run: + +```bash id="a4tuy5" +npm run typecheck +npm run build +npm run audit:pdf +``` + +Add tests where practical for: + +* tab label transformation +* date picker value formatting +* activity status helpers +* audit log display helpers +* follow-up date conversion to YYYY-MM-DD + +--- + +# Manual Verification Scenarios + +## Scenario 1 - Audit Log Tab + +Open quotation detail. + +Expected: + +```text id="1sabcz" +Activity tab is replaced by Audit Log +``` + +Audit rows still show correctly. + +--- + +## Scenario 2 - True Activity + +Create or inspect Activity tab where implemented. + +Expected: + +```text id="ofkfzg" +Shows business tasks, not system logs +``` + +Example: + +```text id="jzae6e" +2026-07-01 Visit customer site +``` + +--- + +## Scenario 3 - Follow-up Date Picker + +Open follow-up form. + +Expected: + +* Date picker opens on click. +* User can select date. +* Selected date displays as YYYY-MM-DD. +* Submitted payload remains compatible with existing API. + +--- + +## Scenario 4 - No Hydration Issue + +Reload detail page. + +Expected: + +* Date displays consistently as YYYY-MM-DD. +* No React hydration warning. + +--- + +# Acceptance Criteria + +* System history tabs are renamed to `Audit Log`. +* Audit Log is read-only. +* Activity is reserved for business tasks and follow-ups. +* CRM Activity foundation exists or is planned with schema/service contracts. +* Follow-up date input uses a calendar date picker. +* Selected date displays as YYYY-MM-DD. +* Existing follow-up APIs remain compatible. +* Existing audit log data still renders correctly. +* Build, typecheck, and PDF audit pass. + +--- + +# Out of Scope + +## Calendar Phase + +* Full calendar module +* Team calendar +* Manager calendar +* Drag-and-drop schedule +* Reminder automation + +## Notification Phase + +* Activity reminder email +* Follow-up due notification +* Overdue notification + +## Migration Phase + +* Full migration of historical follow-up rows into Activity + +--- + +# Final Success Condition + +Task D.6 is complete when CRM system history is consistently shown as `Audit Log`, CRM `Activity` is clearly reserved for MK/Sales business work, and follow-up date entry is improved with a calendar picker using stable `YYYY-MM-DD` formatting. + +--- + +# Implementation Result - 2026-07-01 + +Status: Completed + +Implemented: +- Renamed system-history tabs from `Activity` to `Audit Log` in quotation, lead, opportunity, and customer detail views. +- Added shared `AuditLogTab` UI and reused it across CRM detail surfaces to present immutable audit entries consistently. +- Extended CRM audit-log records with `beforeData`, `afterData`, and `requestId` so the UI can show richer system history. +- Standardized date-picker placeholders to `YYYY-MM-DD` and added clear support in `CrmDateInput`. +- Migrated lead follow-up creation UI from plain text date entry to the shared calendar date-picker pattern. +- Added CRM Activity foundation contracts in `src/features/crm/activity/types.ts` for entity scope, activity types, statuses, priorities, derived overdue status, and future-ready permissions. +- Added permission keys for `crmActivityRead`, `crmActivityCreate`, `crmActivityUpdate`, `crmActivityComplete`, `crmActivityDelete`, and `crmAuditLogRead`. + +Verification: +- `npm run typecheck` -> PASS +- `npm run build` -> PASS +- `npm run audit:pdf` -> PASS + +Notes: +- `npm run build` still reports existing Turbopack warnings about workspace root lockfile detection and an NFT trace warning from PDF audit/template validation paths. Build still completed successfully. +- This task intentionally does not migrate historical follow-up rows into the new activity model yet; follow-ups remain API-compatible and activity contracts are prepared for a later phase. + +Implementation report: +- [task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-d.6-crm-activity-foundation-audit-log-separation-2026-07-01.md) diff --git a/src/components/forms/fields/date-picker-field.tsx b/src/components/forms/fields/date-picker-field.tsx index 2463420..edf0996 100644 --- a/src/components/forms/fields/date-picker-field.tsx +++ b/src/components/forms/fields/date-picker-field.tsx @@ -23,7 +23,7 @@ export function DatePickerField({ label, description, required, - placeholder = 'DD/MM/YYYY', + placeholder = 'YYYY-MM-DD', disabled }: DatePickerFieldProps) { void disabled; diff --git a/src/features/crm/activity/types.ts b/src/features/crm/activity/types.ts new file mode 100644 index 0000000..f7a2b27 --- /dev/null +++ b/src/features/crm/activity/types.ts @@ -0,0 +1,86 @@ +export const CRM_ACTIVITY_ENTITY_TYPES = [ + 'lead', + 'opportunity', + 'quotation', + 'customer', + 'contact', + 'purchase_order', + 'service_report' +] as const; + +export const CRM_ACTIVITY_TYPES = [ + 'site_visit', + 'meeting', + 'phone_call', + 'email_follow_up', + 'quotation_follow_up', + 'site_survey', + 'requirement_discussion', + 'internal_task', + 'other' +] as const; + +export const CRM_ACTIVITY_STATUSES = [ + 'planned', + 'in_progress', + 'completed', + 'cancelled', + 'overdue' +] as const; + +export const CRM_ACTIVITY_PRIORITIES = ['low', 'medium', 'high', 'urgent'] as const; + +export type CrmActivityEntityType = (typeof CRM_ACTIVITY_ENTITY_TYPES)[number]; +export type CrmActivityType = (typeof CRM_ACTIVITY_TYPES)[number]; +export type CrmActivityStatus = (typeof CRM_ACTIVITY_STATUSES)[number]; +export type CrmActivityPriority = (typeof CRM_ACTIVITY_PRIORITIES)[number]; + +export interface CrmActivityRecord { + id: string; + organizationId: string; + entityType: CrmActivityEntityType; + entityId: string; + activityType: CrmActivityType; + subject: string; + description: string | null; + ownerId: string | null; + assignedToId: string | null; + dueDate: string | null; + dueTime: string | null; + completedAt: string | null; + status: Exclude; + priority: CrmActivityPriority; + outcome: string | null; + nextAction: string | null; + createdBy: string; + createdAt: string; + updatedAt: string; +} + +export const CRM_ACTIVITY_PERMISSIONS = { + read: PERMISSIONS.crmActivityRead, + create: PERMISSIONS.crmActivityCreate, + update: PERMISSIONS.crmActivityUpdate, + complete: PERMISSIONS.crmActivityComplete, + delete: PERMISSIONS.crmActivityDelete, + auditLogRead: PERMISSIONS.crmAuditLogRead +} as const; + +export function resolveCrmActivityStatus(input: { + status: Exclude; + dueDate?: string | null; + completedAt?: string | null; + now?: Date; +}): CrmActivityStatus { + if (input.completedAt || input.status === 'completed' || input.status === 'cancelled') { + return input.status; + } + + if (!input.dueDate) { + return input.status; + } + + const today = (input.now ?? new Date()).toISOString().slice(0, 10); + return input.dueDate < today ? 'overdue' : input.status; +} +import { PERMISSIONS } from '@/lib/auth/rbac'; diff --git a/src/features/crm/audit-log/types.ts b/src/features/crm/audit-log/types.ts new file mode 100644 index 0000000..0cd9d48 --- /dev/null +++ b/src/features/crm/audit-log/types.ts @@ -0,0 +1,12 @@ +export interface CrmAuditLogRecord { + id: string; + action: string; + entityType: string; + entityId: string; + createdAt: string; + userId: string; + actorName: string | null; + beforeData: unknown; + afterData: unknown; + requestId: string | null; +} diff --git a/src/features/crm/components/audit-log-tab.tsx b/src/features/crm/components/audit-log-tab.tsx new file mode 100644 index 0000000..f345012 --- /dev/null +++ b/src/features/crm/components/audit-log-tab.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { formatDateTime } from '@/lib/date-format'; +import { cn } from '@/lib/utils'; +import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types'; + +function formatActionLabel(action: string) { + return action.replaceAll('_', ' '); +} + +function formatEntityLabel(entityType: string) { + return entityType.replace(/^crm_/, '').replaceAll('_', ' '); +} + +function normalizeAuditValue(value: unknown) { + if (value === null || value === undefined || value === '') { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function AuditPayloadBlock({ + label, + value +}: { + label: string; + value: unknown; +}) { + const normalizedValue = normalizeAuditValue(value); + + if (!normalizedValue) { + return null; + } + + return ( +
+
{label}
+
+        {normalizedValue}
+      
+
+ ); +} + +export function AuditLogTab({ + title = 'Audit Log', + description = 'System-generated immutable history for this CRM record.', + emptyMessage = 'No audit log recorded yet.', + items, + className +}: { + title?: string; + description?: string; + emptyMessage?: string; + items: CrmAuditLogRecord[]; + className?: string; +}) { + return ( + + + {title} + {description} + + + {!items.length ? ( +
+ {emptyMessage} +
+ ) : ( + items.map((item, index) => ( +
+
+
+ + {formatActionLabel(item.action)} + + + {formatEntityLabel(item.entityType)} + + System +
+
+
{item.actorName ?? item.userId}
+
+ {formatDateTime(item.createdAt)} +
+
+
+ + +
+
+ {index < items.length - 1 ? : null} +
+ )) + )} +
+
+ ); +} diff --git a/src/features/crm/components/crm-form-controls.tsx b/src/features/crm/components/crm-form-controls.tsx index 92feef3..68fbfe5 100644 --- a/src/features/crm/components/crm-form-controls.tsx +++ b/src/features/crm/components/crm-form-controls.tsx @@ -50,41 +50,50 @@ function toDateValue(date?: Date) { export function CrmDateInput({ value, onChange, - placeholder = 'DD/MM/YYYY', - className + placeholder = 'YYYY-MM-DD', + className, + allowClear = true }: { value?: string | null; onChange: (value: string) => void; placeholder?: string; className?: string; + allowClear?: boolean; }) { const selectedDate = parseDateValue(value); return ( - - - + + + onChange(toDateValue(date))} + initialFocus + /> + + + {allowClear && value ? ( + - - - onChange(toDateValue(date))} - initialFocus - /> - - + ) : null} + ); } diff --git a/src/features/crm/customers/api/types.ts b/src/features/crm/customers/api/types.ts index dbda96b..c349dd7 100644 --- a/src/features/crm/customers/api/types.ts +++ b/src/features/crm/customers/api/types.ts @@ -111,15 +111,7 @@ export interface CustomerContactRecord { shares: ContactShareRecord[]; } -export interface CustomerActivityRecord { - id: string; - action: string; - entityType: string; - entityId: string; - createdAt: string; - userId: string; - actorName: string | null; -} +export type CustomerActivityRecord = CrmAuditLogRecord; export interface CustomerRelatedOpportunityItem { id: string; @@ -234,3 +226,4 @@ export interface MutationSuccessResponse { success: boolean; message: string; } +import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types'; diff --git a/src/features/crm/customers/components/customer-detail.tsx b/src/features/crm/customers/components/customer-detail.tsx index 5172910..6adfa13 100644 --- a/src/features/crm/customers/components/customer-detail.tsx +++ b/src/features/crm/customers/components/customer-detail.tsx @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { AuditLogTab } from '@/features/crm/components/audit-log-tab'; import { formatDateTime } from '@/lib/format'; import { formatNumber } from '@/lib/number-format'; import { customerByIdOptions } from '../api/queries'; @@ -129,7 +130,7 @@ export function CustomerDetail({ ภาพรวม {canReadContacts ? ผู้ติดต่อ : null} - กิจกรรม + Audit Log เอกสารที่เกี่ยวข้อง @@ -194,41 +195,11 @@ export function CustomerDetail({ ) : null} - - - กิจกรรม - ประวัติการเปลี่ยนแปลงที่เกิดจากการแก้ไขข้อมูลลูกค้า - - - {!data.activity.length ? ( -
- ยังไม่มีกิจกรรม -
- ) : ( - data.activity.map((item, index) => ( -
-
-
-
-
- - {item.action} - - - {item.actorName ?? item.userId} - -
-
- {formatDateTime(item.createdAt)} -
-
-
- {index < data.activity.length - 1 ? : null} -
- )) - )} - - + diff --git a/src/features/crm/customers/server/service.ts b/src/features/crm/customers/server/service.ts index 4ac691c..929562f 100644 --- a/src/features/crm/customers/server/service.ts +++ b/src/features/crm/customers/server/service.ts @@ -1033,6 +1033,9 @@ export async function getCustomerActivity( createdAt: log.createdAt, userId: log.userId, actorName: actorMap.get(log.userId) ?? null, + beforeData: log.beforeData, + afterData: log.afterData, + requestId: log.requestId, })); } diff --git a/src/features/crm/leads/components/lead-detail.tsx b/src/features/crm/leads/components/lead-detail.tsx index f1c4bfe..014b984 100644 --- a/src/features/crm/leads/components/lead-detail.tsx +++ b/src/features/crm/leads/components/lead-detail.tsx @@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { AuditLogTab } from '@/features/crm/components/audit-log-tab'; import { formatDateTime } from '@/lib/date-format'; import { formatNumber } from '@/lib/number-format'; import { leadByIdOptions } from '../api/queries'; @@ -111,7 +112,7 @@ export function LeadDetail({ Follow-ups Linked Opportunities - Activity + Audit Log @@ -152,26 +153,11 @@ export function LeadDetail({ - - - Activity - - - {lead.activity.length === 0 ? ( -
No activity yet.
- ) : ( - lead.activity.map((activity) => ( -
-
{activity.action}
-
- {activity.actorName ?? activity.userId} -{' '} - {formatDateTime(activity.createdAt)} -
-
- )) - )} -
-
+
diff --git a/src/features/crm/leads/components/lead-followup-panel.tsx b/src/features/crm/leads/components/lead-followup-panel.tsx index 9ccb983..ed19ccd 100644 --- a/src/features/crm/leads/components/lead-followup-panel.tsx +++ b/src/features/crm/leads/components/lead-followup-panel.tsx @@ -23,7 +23,15 @@ export function LeadFollowupPanel({ leadId: string; detail: LeadDetailResponse; }) { - const { FormTextField, FormSelectField, FormTextareaField } = useFormFields(); + const { FormDatePickerField, FormSelectField, FormTextareaField } = + useFormFields(); + + const mutation = useMutation({ + ...createLeadFollowupMutation, + onError: (error) => { + toast.error(error instanceof Error ? error.message : 'Unable to save lead follow-up'); + } + }); const form = useAppForm({ defaultValues: { @@ -42,80 +50,78 @@ export function LeadFollowupPanel({ nextFollowupDate: value.nextFollowupDate || null } }); - } - }); - - const mutation = useMutation({ - ...createLeadFollowupMutation, - onSuccess: () => { - toast.success('บันทึก follow-up สำเร็จ'); + toast.success('Saved lead follow-up successfully'); form.reset(); - }, - onError: (error) => { - toast.error(error instanceof Error ? error.message : 'ไม่สามารถบันทึก follow-up ได้'); } }); return ( -
- - - New Follow-up - - - - - - + + Follow-ups + + + + + + + ({ + value: item.id, + label: item.label + }))} + /> +
+ - ({ - value: item.id, - label: item.label - }))} - /> -
- -
- -
- - - - +
+ + - - - History - - +
+ +
+ +
{detail.followups.length === 0 ? ( -
No follow-up history yet.
+
+ No follow-ups yet. +
) : ( detail.followups.map((followup) => ( -
-
-
- {followup.followupStatusLabel ?? followup.followupStatus} +
+
{followup.followupStatusLabel ?? followup.followupStatus}
+
+ {formatDate(followup.followupDate)} +
+
{followup.note || '-'}
+ {followup.nextFollowupDate ? ( +
+ Next: {formatDate(followup.nextFollowupDate)}
-
{formatDate(followup.followupDate)}
-
- {followup.note ?
{followup.note}
: null} -
- {followup.createdByName ?? followup.createdBy} -
+ ) : null}
)) )} - - -
+
+ + ); } diff --git a/src/features/crm/leads/server/service.ts b/src/features/crm/leads/server/service.ts index e6518e5..0d8e3ba 100644 --- a/src/features/crm/leads/server/service.ts +++ b/src/features/crm/leads/server/service.ts @@ -258,7 +258,10 @@ async function listLeadActivity( entityId: log.entityId, createdAt: log.createdAt, userId: log.userId, - actorName: actorMap.get(log.userId) ?? null + actorName: actorMap.get(log.userId) ?? null, + beforeData: log.beforeData, + afterData: log.afterData, + requestId: log.requestId })); } @@ -1077,4 +1080,3 @@ export function buildLeadAccessContext(input: { }) { return buildCrmSecurityContext(input); } - diff --git a/src/features/crm/leads/types.ts b/src/features/crm/leads/types.ts index 4dec438..ace991b 100644 --- a/src/features/crm/leads/types.ts +++ b/src/features/crm/leads/types.ts @@ -47,15 +47,7 @@ export interface LeadLinkedOpportunitySummary { updatedAt: string; } -export interface LeadActivityRecord { - id: string; - action: string; - entityType: string; - entityId: string; - createdAt: string; - userId: string; - actorName: string | null; -} +export type LeadActivityRecord = CrmAuditLogRecord; export interface LeadReferenceData { awarenesses: LeadOption[]; @@ -225,4 +217,5 @@ export interface LeadMutationSuccessResponse { lead?: LeadDetail; followup?: LeadFollowupSummary; } +import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types'; diff --git a/src/features/crm/opportunities/api/types.ts b/src/features/crm/opportunities/api/types.ts index aec162d..bd29c48 100644 --- a/src/features/crm/opportunities/api/types.ts +++ b/src/features/crm/opportunities/api/types.ts @@ -166,15 +166,7 @@ export interface OpportunityFollowupRecord { updatedBy: string; } -export interface OpportunityActivityRecord { - id: string; - action: string; - entityType: string; - entityId: string; - createdAt: string; - userId: string; - actorName: string | null; -} +export type OpportunityActivityRecord = CrmAuditLogRecord; export interface OpportunityReferenceData { branches: OpportunityBranchOption[]; @@ -337,3 +329,4 @@ export interface MutationSuccessResponse { success: boolean; message: string; } +import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types'; diff --git a/src/features/crm/opportunities/components/opportunity-detail.tsx b/src/features/crm/opportunities/components/opportunity-detail.tsx index 2414b67..21ccc49 100644 --- a/src/features/crm/opportunities/components/opportunity-detail.tsx +++ b/src/features/crm/opportunities/components/opportunity-detail.tsx @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { AuditLogTab } from '@/features/crm/components/audit-log-tab'; import { formatDate, formatDateTime } from '@/lib/date-format'; import { formatNumber } from '@/lib/number-format'; import type { @@ -284,7 +285,7 @@ export function OpportunityDetail({ Follow-up Timeline Linked Quotations - Activity + Audit Log @@ -331,26 +332,11 @@ export function OpportunityDetail({ - - - Activity - - - {data.activity.length === 0 ? ( -
No activity yet.
- ) : ( - data.activity.map((activity) => ( -
-
{activity.action}
-
- {activity.actorName ?? activity.userId} -{' '} - {formatDateTime(activity.createdAt)} -
-
- )) - )} -
-
+
diff --git a/src/features/crm/opportunities/server/service.ts b/src/features/crm/opportunities/server/service.ts index 00156c6..98d11eb 100644 --- a/src/features/crm/opportunities/server/service.ts +++ b/src/features/crm/opportunities/server/service.ts @@ -1136,7 +1136,10 @@ export async function getOpportunityActivity( entityId: log.entityId, createdAt: log.createdAt, userId: log.userId, - actorName: actorMap.get(log.userId) ?? null + actorName: actorMap.get(log.userId) ?? null, + beforeData: log.beforeData, + afterData: log.afterData, + requestId: log.requestId })); } diff --git a/src/features/crm/quotations/api/types.ts b/src/features/crm/quotations/api/types.ts index eca6ab4..1727277 100644 --- a/src/features/crm/quotations/api/types.ts +++ b/src/features/crm/quotations/api/types.ts @@ -257,15 +257,7 @@ export interface QuotationAttachmentRecord { deletedAt: string | null; } -export interface QuotationActivityRecord { - id: string; - action: string; - entityType: string; - entityId: string; - createdAt: string; - userId: string; - actorName: string | null; -} +export type QuotationActivityRecord = CrmAuditLogRecord; export interface QuotationRelationItem { id: string; @@ -481,3 +473,4 @@ export interface QuotationCustomerPackageGenerateResponse { pageCount: number; } | null; } +import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types'; diff --git a/src/features/crm/quotations/components/quotation-detail.tsx b/src/features/crm/quotations/components/quotation-detail.tsx index 8e1e24f..735e336 100644 --- a/src/features/crm/quotations/components/quotation-detail.tsx +++ b/src/features/crm/quotations/components/quotation-detail.tsx @@ -27,6 +27,7 @@ import { SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { AuditLogTab } from '@/features/crm/components/audit-log-tab'; import { formatDate, formatDateTime } from '@/lib/date-format'; import { formatNumber } from '@/lib/number-format'; import { @@ -1231,7 +1232,7 @@ const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes( Topics Follow-ups Attachments - Activity + Audit Log Approval @@ -1657,45 +1658,12 @@ const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
- - - - Activity - - Audit events for quotation mutations and child records. - - - - {!data.activity.length ? ( - - ) : ( - data.activity.map((item, index) => ( -
-
-
-
-
- - {item.action} - - - {item.entityType.replaceAll('_', ' ')} - - - {item.actorName ?? item.userId} - -
-
- {formatDateTime(item.createdAt)} -
-
-
- {index < data.activity.length - 1 ? : null} -
- )) - )} - - + + diff --git a/src/features/crm/quotations/server/service.ts b/src/features/crm/quotations/server/service.ts index 578845a..395de8a 100644 --- a/src/features/crm/quotations/server/service.ts +++ b/src/features/crm/quotations/server/service.ts @@ -1444,7 +1444,10 @@ export async function getQuotationActivity( entityId: log.entityId, createdAt: log.createdAt, userId: log.userId, - actorName: actorMap.get(log.userId) ?? null + actorName: actorMap.get(log.userId) ?? null, + beforeData: log.beforeData, + afterData: log.afterData, + requestId: log.requestId })); } diff --git a/src/lib/auth/rbac.ts b/src/lib/auth/rbac.ts index c8a3573..e2bf3d6 100644 --- a/src/lib/auth/rbac.ts +++ b/src/lib/auth/rbac.ts @@ -39,6 +39,12 @@ export const PERMISSIONS = { crmLeadRead: 'crm.lead.read', crmLeadCreate: 'crm.lead.create', crmLeadUpdate: 'crm.lead.update', + crmActivityRead: 'crm.activity.read', + crmActivityCreate: 'crm.activity.create', + crmActivityUpdate: 'crm.activity.update', + crmActivityComplete: 'crm.activity.complete', + crmActivityDelete: 'crm.activity.delete', + crmAuditLogRead: 'crm.audit_log.read', crmLeadAssign: 'crm.lead.assign', crmLeadDelete: 'crm.lead.delete', crmCustomerRead: 'crm.customer.read', @@ -513,6 +519,18 @@ PERMISSIONS.crmDocumentTemplateRead, }; export const CRM_PERMISSION_GROUPS: PermissionGroup[] = [ + { + key: 'activity', + label: 'Activities & Audit', + permissions: [ + { key: PERMISSIONS.crmActivityRead, label: 'Read activities' }, + { key: PERMISSIONS.crmActivityCreate, label: 'Create activities' }, + { key: PERMISSIONS.crmActivityUpdate, label: 'Update activities' }, + { key: PERMISSIONS.crmActivityComplete, label: 'Complete activities' }, + { key: PERMISSIONS.crmActivityDelete, label: 'Delete activities' }, + { key: PERMISSIONS.crmAuditLogRead, label: 'Read audit logs' } + ] + }, { key: 'lead', label: 'Leads',