This commit is contained in:
phaichayon
2026-07-01 08:44:35 +07:00
parent 0c39472dca
commit 6d6cd3a6df
32 changed files with 1284 additions and 333 deletions

View File

@@ -1,10 +1,10 @@
{ {
"audit": "payload-parity", "audit": "payload-parity",
"overallStatus": "PASS", "overallStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"previewHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", "previewHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6",
"generationHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", "generationHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6",
"snapshotHash": "8f93479501b66dfd397c742447b010eaf427697429bfcfd523cd127d06106f26", "snapshotHash": "dd0c565a00c6161d66e3286380ca41eec17a2eee1f183236644733141cd6c1e6",
"mismatches": [], "mismatches": [],
"comparableFieldCount": 31, "comparableFieldCount": 31,
"topicInputKeyCount": 6, "topicInputKeyCount": 6,

View File

@@ -2,7 +2,7 @@
"audit": "runtime-payload", "audit": "runtime-payload",
"overallStatus": "PASS", "overallStatus": "PASS",
"staticLabelsStatus": "PASS", "staticLabelsStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"templateName": "ALLA Demo Quotation Standard", "templateName": "ALLA Demo Quotation Standard",
"templateVersion": "1.0", "templateVersion": "1.0",
"findingCount": 168, "findingCount": 168,
@@ -25,28 +25,28 @@
"field": "documentData.customer.id", "field": "documentData.customer.id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "cf2c1f29-ce0a-41dd-acc0-13b7f82086cd", "value": "a476b13f-87f5-47dc-9606-e88ec24cda87",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.customer.name", "field": "documentData.customer.name",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ", "value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.customer.address", "field": "documentData.customer.address",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Bang Sue, Bangkok", "value": "Mueang Chiang Mai, Chiang Mai",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.customer.phone", "field": "documentData.customer.phone",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "02-400827", "value": "02-400824",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -60,14 +60,14 @@
"field": "documentData.contact.id", "field": "documentData.contact.id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "7c88a108-419f-47ed-a62b-1d784a7b8455", "value": "78c92957-126f-4765-873f-14ce01330be9",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.contact.name", "field": "documentData.contact.name",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "คุณวราภรณ์ เกษมศรี", "value": "คุณกิตติพงศ์ สุวรรณ",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -81,56 +81,56 @@
"field": "documentData.contact.mobile", "field": "documentData.contact.mobile",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "0810000250", "value": "0810000220",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.contact.position", "field": "documentData.contact.position",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Procurement Manager", "value": "Plant Manager",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.id", "field": "documentData.quotation.id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "0bbb3fce-f3a2-408a-b93a-6faaa4d74f55", "value": "c0a3c490-30ae-4c77-8f80-ec9328eeb66e",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.code", "field": "documentData.quotation.code",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "CRA2606-1108", "value": "CRA2606-1107",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.projectName", "field": "documentData.quotation.projectName",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Material Handling Lab Crane", "value": "Dealer Stock Crane Package",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.projectLocation", "field": "documentData.quotation.projectLocation",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Bang Sue Engineering Faculty", "value": "Chiang Mai Service Warehouse",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.attention", "field": "documentData.quotation.attention",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "คุณวราภรณ์ เกษมศรี", "value": "คุณกิตติพงศ์ สุวรรณ",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotation.reference", "field": "documentData.quotation.reference",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "crm-uat-seed-v1:university-lab-crane-approved", "value": "crm-uat-seed-v1:dealer-channel-approved",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -151,14 +151,14 @@
"field": "documentData.quotation.subtotal", "field": "documentData.quotation.subtotal",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 3008000, "value": 4418000,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
"field": "documentData.quotation.totalAmount", "field": "documentData.quotation.totalAmount",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 3122003, "value": 4585442,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -172,7 +172,7 @@
"field": "documentData.items[0].id", "field": "documentData.items[0].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "f37f7d99-f1e8-4aa9-a207-0a280ec74e26", "value": "7ffd6a54-7c9f-4ab2-b6b2-7a38a0f680e9",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -186,7 +186,7 @@
"field": "documentData.items[0].description", "field": "documentData.items[0].description",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Main equipment supply for Material Handling Lab Crane", "value": "Main equipment supply for Dealer Stock Crane Package",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -207,7 +207,7 @@
"field": "documentData.items[0].unitPrice", "field": "documentData.items[0].unitPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 1717102, "value": 2521993,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -221,14 +221,14 @@
"field": "documentData.items[0].totalPrice", "field": "documentData.items[0].totalPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 1717102, "value": 2521993,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
"field": "documentData.items[1].id", "field": "documentData.items[1].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "63809391-7dd6-4ebd-8aba-3cb5d18b148a", "value": "02c15bda-11ae-4e5c-a0f4-a5943214abec",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -242,7 +242,7 @@
"field": "documentData.items[1].description", "field": "documentData.items[1].description",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Installation and commissioning for Material Handling Lab Crane", "value": "Installation and commissioning for Dealer Stock Crane Package",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -263,7 +263,7 @@
"field": "documentData.items[1].unitPrice", "field": "documentData.items[1].unitPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 874161, "value": 1283924,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -277,14 +277,14 @@
"field": "documentData.items[1].totalPrice", "field": "documentData.items[1].totalPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 874161, "value": 1283924,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
"field": "documentData.items[2].id", "field": "documentData.items[2].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "6e304b78-bbb6-4beb-b04e-6958a9e3ca69", "value": "d13a4d65-7b45-46d5-a2d1-2f79db82779b",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -298,7 +298,7 @@
"field": "documentData.items[2].description", "field": "documentData.items[2].description",
"source": "documentData", "source": "documentData",
"status": "PASS", "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" "message": "String is populated"
}, },
{ {
@@ -319,7 +319,7 @@
"field": "documentData.items[2].unitPrice", "field": "documentData.items[2].unitPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 312200, "value": 458544,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -333,14 +333,14 @@
"field": "documentData.items[2].totalPrice", "field": "documentData.items[2].totalPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 312200, "value": 458544,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
"field": "documentData.items[3].id", "field": "documentData.items[3].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "e28c0f75-086f-4d4a-bc81-a3292fce97b2", "value": "970d78d6-939f-4856-a17f-3538983f149a",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -354,7 +354,7 @@
"field": "documentData.items[3].description", "field": "documentData.items[3].description",
"source": "documentData", "source": "documentData",
"status": "PASS", "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" "message": "String is populated"
}, },
{ {
@@ -375,7 +375,7 @@
"field": "documentData.items[3].unitPrice", "field": "documentData.items[3].unitPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 218540, "value": 320981,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -389,7 +389,7 @@
"field": "documentData.items[3].totalPrice", "field": "documentData.items[3].totalPrice",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": 218540, "value": 320981,
"message": "Scalar value is populated" "message": "Scalar value is populated"
}, },
{ {
@@ -403,14 +403,14 @@
"field": "documentData.quotationCustomers[0].id", "field": "documentData.quotationCustomers[0].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "3eb300e9-8410-4a97-b2f3-2fb935e60a0b", "value": "40ef3613-a862-4166-be00-b79237e80a68",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.quotationCustomers[0].customerId", "field": "documentData.quotationCustomers[0].customerId",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "cf2c1f29-ce0a-41dd-acc0-13b7f82086cd", "value": "a476b13f-87f5-47dc-9606-e88ec24cda87",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -438,7 +438,7 @@
"field": "documentData.topics.all[0].id", "field": "documentData.topics.all[0].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "3013a775-bc86-4b22-90f9-048fa1225a91", "value": "7fdc1ec7-fd3b-479c-8688-913c4e54329b",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -473,7 +473,7 @@
"field": "documentData.topics.all[0].items[0].content", "field": "documentData.topics.all[0].items[0].content",
"source": "documentData", "source": "documentData",
"status": "PASS", "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" "message": "String is populated"
}, },
{ {
@@ -501,7 +501,7 @@
"field": "documentData.topics.all[1].id", "field": "documentData.topics.all[1].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "bf614822-2e02-456e-af35-c9dc50b0140f", "value": "921523e3-c2a5-4be1-8d10-a2a4c2913176",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -564,7 +564,7 @@
"field": "documentData.topics.all[2].id", "field": "documentData.topics.all[2].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "eee9e6f9-15d5-4540-9cce-628026bc0281", "value": "b3138a62-36c3-4ffb-9504-08479aa3d263",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -634,7 +634,7 @@
"field": "documentData.topics.exclusion[0].id", "field": "documentData.topics.exclusion[0].id",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "bf614822-2e02-456e-af35-c9dc50b0140f", "value": "921523e3-c2a5-4be1-8d10-a2a4c2913176",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -732,14 +732,14 @@
"field": "documentData.pdfme.quotation_date", "field": "documentData.pdfme.quotation_date",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Jun 15, 2026", "value": "Jun 3, 2026",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.pdfme.quotation_price", "field": "documentData.pdfme.quotation_price",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "THB 3,008,000.00", "value": "THB 4,418,000.00",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -816,70 +816,70 @@
"field": "documentData.signatures.preparedBy.actedAt", "field": "documentData.signatures.preparedBy.actedAt",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "2026-06-15T09:00:00.000Z", "value": "2026-06-03T09:00:00.000Z",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.approvedBy.name", "field": "documentData.signatures.approvedBy.name",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "-", "value": "Sales Manager UAT",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.approvedBy.position", "field": "documentData.signatures.approvedBy.position",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "-", "value": "Sales Manager",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.approvedBy.actedAt", "field": "documentData.signatures.approvedBy.actedAt",
"source": "documentData", "source": "documentData",
"status": "WARNING", "status": "PASS",
"value": null, "value": "2026-06-06T10:00:00.000Z",
"message": "Value is null" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.authorizedBy.name", "field": "documentData.signatures.authorizedBy.name",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Sales Manager UAT", "value": "CEO UAT",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.authorizedBy.position", "field": "documentData.signatures.authorizedBy.position",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "Sales Manager", "value": "General Manager",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "documentData.signatures.authorizedBy.actedAt", "field": "documentData.signatures.authorizedBy.actedAt",
"source": "documentData", "source": "documentData",
"status": "PASS", "status": "PASS",
"value": "2026-06-17T11:00:00.000Z", "value": "2026-06-07T09:00:00.000Z",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.customer_name", "field": "templateInput.customer_name",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ", "value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.customer_addr", "field": "templateInput.customer_addr",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Bang Sue, Bangkok", "value": "Mueang Chiang Mai, Chiang Mai",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.customer_tel", "field": "templateInput.customer_tel",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "02-400827", "value": "02-400824",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -893,35 +893,35 @@
"field": "templateInput.customer_att", "field": "templateInput.customer_att",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "คุณวราภรณ์ เกษมศรี", "value": "คุณกิตติพงศ์ สุวรรณ",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.project_name", "field": "templateInput.project_name",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Material Handling Lab Crane", "value": "Dealer Stock Crane Package",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.site_location", "field": "templateInput.site_location",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Bang Sue Engineering Faculty", "value": "Chiang Mai Service Warehouse",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.quotation_date_data", "field": "templateInput.quotation_date_data",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Jun 15, 2026", "value": "Jun 3, 2026",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.quotation_code_data", "field": "templateInput.quotation_code_data",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "CRA2606-1108", "value": "CRA2606-1107",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -935,7 +935,7 @@
"field": "templateInput.quotation_price", "field": "templateInput.quotation_price",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "THB 3,008,000.00", "value": "THB 4,418,000.00",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -998,35 +998,35 @@
"field": "templateInput.app2", "field": "templateInput.app2",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "-", "value": "Sales Manager UAT",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.app2_position", "field": "templateInput.app2_position",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "-", "value": "Sales Manager",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.app3", "field": "templateInput.app3",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Sales Manager UAT", "value": "CEO UAT",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.app3_position", "field": "templateInput.app3_position",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Sales Manager", "value": "General Manager",
"message": "String is populated" "message": "String is populated"
}, },
{ {
"field": "templateInput.quotation_date", "field": "templateInput.quotation_date",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "Jun 15, 2026", "value": "Jun 3, 2026",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -1075,7 +1075,7 @@
"field": "templateInput.quotation_code", "field": "templateInput.quotation_code",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "CRA2606-1108", "value": "CRA2606-1107",
"message": "String is populated" "message": "String is populated"
}, },
{ {
@@ -1138,7 +1138,7 @@
"field": "templateInput.quotation_price_data[0][1]", "field": "templateInput.quotation_price_data[0][1]",
"source": "templateInput", "source": "templateInput",
"status": "PASS", "status": "PASS",
"value": "฿3,008,000.00", "value": "฿4,418,000.00",
"message": "Price table amount cell is formatted" "message": "Price table amount cell is formatted"
}, },
{ {

View File

@@ -1,14 +1,14 @@
{ {
"audit": "approved-snapshot-parity", "audit": "approved-snapshot-parity",
"overallStatus": "PASS", "overallStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"approvedSnapshotAvailable": true, "approvedSnapshotAvailable": true,
"approvedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84", "approvedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84",
"computedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84", "computedTemplateVersionId": "e7e89bf2-f107-4f90-b03d-4a7533883d84",
"inconsistentTemplateKeys": [], "inconsistentTemplateKeys": [],
"computedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229", "computedSignatureHash": "4106a8d9883d207231103e13f29aff692e6ad1d81a0e61cb26cea7ceea73e3b8",
"storedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229", "storedSignatureHash": "4106a8d9883d207231103e13f29aff692e6ad1d81a0e61cb26cea7ceea73e3b8",
"approvedArtifactReference": null, "approvedArtifactReference": "artifact:7088f4e9-c56a-4389-975b-3c7ccf13c772",
"refreshedFixtureSnapshot": true, "refreshedFixtureSnapshot": true,
"usedAuditFixture": false "usedAuditFixture": false
} }

View File

@@ -1,7 +1,7 @@
{ {
"generatedAt": "2026-06-30T16:18:58.393Z", "generatedAt": "2026-06-30T23:54:34.472Z",
"overallStatus": "PASS", "overallStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"artifacts": { "artifacts": {
"templateVersionReport": "template-version-report.json", "templateVersionReport": "template-version-report.json",
"placeholderIntegrityReport": "placeholder-integrity-report.json", "placeholderIntegrityReport": "placeholder-integrity-report.json",

View File

@@ -1,8 +1,8 @@
# PDF Integrity Summary # PDF Integrity Summary
- Generated At: 2026-06-30T16:18:58.393Z - Generated At: 2026-06-30T23:54:34.472Z
- Overall Status: PASS - Overall Status: PASS
- Quotation Code: CRA2606-1108 - Quotation Code: CRA2606-1107
## Statuses ## Statuses

View File

@@ -2,7 +2,7 @@
"audit": "template-version-integrity", "audit": "template-version-integrity",
"overallStatus": "PASS", "overallStatus": "PASS",
"activeTemplateCount": 1, "activeTemplateCount": 1,
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"approvedTemplateVersionId": null, "approvedTemplateVersionId": null,
"approvedTemplateVersionIsActive": false, "approvedTemplateVersionIsActive": false,
"issues": [] "issues": []

View File

@@ -1,7 +1,7 @@
{ {
"audit": "topic-runtime", "audit": "topic-runtime",
"overallStatus": "PASS", "overallStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"topicCount": 3, "topicCount": 3,
"topicInputKeys": [ "topicInputKeys": [
"topic_1_0", "topic_1_0",

View File

@@ -1,7 +1,7 @@
{ {
"generatedAt": "2026-06-30T16:18:57.428Z", "generatedAt": "2026-06-30T23:54:26.582Z",
"overallStatus": "PASS", "overallStatus": "PASS",
"quotationCode": "CRA2606-1108", "quotationCode": "CRA2606-1107",
"expectedFixtureCode": "QT-H5-AUDIT", "expectedFixtureCode": "QT-H5-AUDIT",
"template": { "template": {
"templateName": "ALLA Demo Quotation Standard", "templateName": "ALLA Demo Quotation Standard",
@@ -263,7 +263,7 @@
"consistency": { "consistency": {
"status": "PASS", "status": "PASS",
"approvedSnapshotAvailable": true, "approvedSnapshotAvailable": true,
"approvedArtifactReference": null, "approvedArtifactReference": "artifact:7088f4e9-c56a-4389-975b-3c7ccf13c772",
"inconsistentKeys": [] "inconsistentKeys": []
}, },
"integritySummary": { "integritySummary": {

View File

@@ -1,8 +1,8 @@
# PDF Audit Report # PDF Audit Report
- Generated At: 2026-06-30T16:18:57.428Z - Generated At: 2026-06-30T23:54:26.582Z
- Overall Status: PASS - Overall Status: PASS
- Quotation Code: CRA2606-1108 - Quotation Code: CRA2606-1107
- Template: ALLA Demo Quotation Standard v1.0 - Template: ALLA Demo Quotation Standard v1.0
- Previous Version: None - Previous Version: None

View File

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

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

127
plans/task-d.6.1.md Normal file
View File

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

642
plans/task-d.6.md Normal file
View File

@@ -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"
<AuditLogTab />
```
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)

View File

@@ -23,7 +23,7 @@ export function DatePickerField({
label, label,
description, description,
required, required,
placeholder = 'DD/MM/YYYY', placeholder = 'YYYY-MM-DD',
disabled disabled
}: DatePickerFieldProps) { }: DatePickerFieldProps) {
void disabled; void disabled;

View File

@@ -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<CrmActivityStatus, 'overdue'>;
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<CrmActivityStatus, 'overdue'>;
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';

View File

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

View File

@@ -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 (
<div className='flex flex-col gap-2 rounded-lg border bg-muted/30 p-3'>
<div className='text-muted-foreground text-xs font-medium'>{label}</div>
<pre className='overflow-x-auto whitespace-pre-wrap break-words text-xs'>
{normalizedValue}
</pre>
</div>
);
}
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 (
<Card className={className}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className='flex flex-col gap-4'>
{!items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
{emptyMessage}
</div>
) : (
items.map((item, index) => (
<div key={item.id} className='flex flex-col gap-4'>
<div className='flex flex-col gap-3 rounded-lg border p-4'>
<div className='flex flex-wrap items-center gap-2'>
<Badge variant='outline' className='capitalize'>
{formatActionLabel(item.action)}
</Badge>
<Badge variant='secondary' className='capitalize'>
{formatEntityLabel(item.entityType)}
</Badge>
<Badge variant='secondary'>System</Badge>
</div>
<div className='flex flex-col gap-1 text-sm'>
<div className='font-medium'>{item.actorName ?? item.userId}</div>
<div className='text-muted-foreground text-xs'>
{formatDateTime(item.createdAt)}
</div>
</div>
<div
className={cn(
'grid gap-3',
normalizeAuditValue(item.beforeData) && normalizeAuditValue(item.afterData)
? 'md:grid-cols-2'
: 'md:grid-cols-1'
)}
>
<AuditPayloadBlock label='Before' value={item.beforeData} />
<AuditPayloadBlock label='After' value={item.afterData} />
</div>
</div>
{index < items.length - 1 ? <Separator /> : null}
</div>
))
)}
</CardContent>
</Card>
);
}

View File

@@ -50,26 +50,28 @@ function toDateValue(date?: Date) {
export function CrmDateInput({ export function CrmDateInput({
value, value,
onChange, onChange,
placeholder = 'DD/MM/YYYY', placeholder = 'YYYY-MM-DD',
className className,
allowClear = true
}: { }: {
value?: string | null; value?: string | null;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
allowClear?: boolean;
}) { }) {
const selectedDate = parseDateValue(value); const selectedDate = parseDateValue(value);
return ( return (
<div className={cn('flex items-center gap-2', className)}>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
className={cn( className={cn(
'w-full justify-start overflow-hidden text-left font-normal', 'min-w-0 flex-1 justify-start overflow-hidden text-left font-normal',
!value && 'text-muted-foreground', !value && 'text-muted-foreground'
className
)} )}
> >
<Icons.calendar className='mr-2 h-4 w-4 shrink-0' /> <Icons.calendar className='mr-2 h-4 w-4 shrink-0' />
@@ -85,6 +87,13 @@ export function CrmDateInput({
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{allowClear && value ? (
<Button type='button' variant='outline' size='icon' onClick={() => onChange('')}>
<Icons.close />
<span className='sr-only'>Clear date</span>
</Button>
) : null}
</div>
); );
} }

View File

@@ -111,15 +111,7 @@ export interface CustomerContactRecord {
shares: ContactShareRecord[]; shares: ContactShareRecord[];
} }
export interface CustomerActivityRecord { export type CustomerActivityRecord = CrmAuditLogRecord;
id: string;
action: string;
entityType: string;
entityId: string;
createdAt: string;
userId: string;
actorName: string | null;
}
export interface CustomerRelatedOpportunityItem { export interface CustomerRelatedOpportunityItem {
id: string; id: string;
@@ -234,3 +226,4 @@ export interface MutationSuccessResponse {
success: boolean; success: boolean;
message: string; message: string;
} }
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';

View File

@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AuditLogTab } from '@/features/crm/components/audit-log-tab';
import { formatDateTime } from '@/lib/format'; import { formatDateTime } from '@/lib/format';
import { formatNumber } from '@/lib/number-format'; import { formatNumber } from '@/lib/number-format';
import { customerByIdOptions } from '../api/queries'; import { customerByIdOptions } from '../api/queries';
@@ -129,7 +130,7 @@ export function CustomerDetail({
<TabsList> <TabsList>
<TabsTrigger value='overview'></TabsTrigger> <TabsTrigger value='overview'></TabsTrigger>
{canReadContacts ? <TabsTrigger value='contacts'></TabsTrigger> : null} {canReadContacts ? <TabsTrigger value='contacts'></TabsTrigger> : null}
<TabsTrigger value='activity'></TabsTrigger> <TabsTrigger value='activity'>Audit Log</TabsTrigger>
<TabsTrigger value='related'></TabsTrigger> <TabsTrigger value='related'></TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='overview'> <TabsContent value='overview'>
@@ -194,41 +195,11 @@ export function CustomerDetail({
</TabsContent> </TabsContent>
) : null} ) : null}
<TabsContent value='activity'> <TabsContent value='activity'>
<Card> <AuditLogTab
<CardHeader> items={data.activity}
<CardTitle></CardTitle> emptyMessage='No audit log recorded yet.'
<CardDescription></CardDescription> description='System-generated immutable history for this customer.'
</CardHeader> />
<CardContent className='space-y-4'>
{!data.activity.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
</div>
) : (
data.activity.map((item, index) => (
<div key={item.id} className='space-y-4'>
<div className='flex items-start gap-3'>
<div className='bg-primary mt-2 h-2 w-2 rounded-full' />
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<Badge variant='outline' className='capitalize'>
{item.action}
</Badge>
<span className='text-sm font-medium'>
{item.actorName ?? item.userId}
</span>
</div>
<div className='text-muted-foreground text-xs'>
{formatDateTime(item.createdAt)}
</div>
</div>
</div>
{index < data.activity.length - 1 ? <Separator /> : null}
</div>
))
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
<TabsContent value='related'> <TabsContent value='related'>
<Card> <Card>

View File

@@ -1033,6 +1033,9 @@ export async function getCustomerActivity(
createdAt: log.createdAt, createdAt: log.createdAt,
userId: log.userId, userId: log.userId,
actorName: actorMap.get(log.userId) ?? null, actorName: actorMap.get(log.userId) ?? null,
beforeData: log.beforeData,
afterData: log.afterData,
requestId: log.requestId,
})); }));
} }

View File

@@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 { formatDateTime } from '@/lib/date-format';
import { formatNumber } from '@/lib/number-format'; import { formatNumber } from '@/lib/number-format';
import { leadByIdOptions } from '../api/queries'; import { leadByIdOptions } from '../api/queries';
@@ -111,7 +112,7 @@ export function LeadDetail({
<TabsList> <TabsList>
<TabsTrigger value='followups'>Follow-ups</TabsTrigger> <TabsTrigger value='followups'>Follow-ups</TabsTrigger>
<TabsTrigger value='linked-opportunities'>Linked Opportunities</TabsTrigger> <TabsTrigger value='linked-opportunities'>Linked Opportunities</TabsTrigger>
<TabsTrigger value='activity'>Activity</TabsTrigger> <TabsTrigger value='activity'>Audit Log</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='followups'> <TabsContent value='followups'>
@@ -152,26 +153,11 @@ export function LeadDetail({
</TabsContent> </TabsContent>
<TabsContent value='activity'> <TabsContent value='activity'>
<Card> <AuditLogTab
<CardHeader> items={lead.activity}
<CardTitle>Activity</CardTitle> emptyMessage='No audit log recorded yet.'
</CardHeader> description='System-generated immutable history for this lead.'
<CardContent className='space-y-3'> />
{lead.activity.length === 0 ? (
<div className='text-muted-foreground text-sm'>No activity yet.</div>
) : (
lead.activity.map((activity) => (
<div key={activity.id} className='border-border rounded-md border p-3'>
<div className='font-medium'>{activity.action}</div>
<div className='text-muted-foreground mt-1 text-xs'>
{activity.actorName ?? activity.userId} -{' '}
{formatDateTime(activity.createdAt)}
</div>
</div>
))
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@@ -23,7 +23,15 @@ export function LeadFollowupPanel({
leadId: string; leadId: string;
detail: LeadDetailResponse; detail: LeadDetailResponse;
}) { }) {
const { FormTextField, FormSelectField, FormTextareaField } = useFormFields<FollowupFormValues>(); const { FormDatePickerField, FormSelectField, FormTextareaField } =
useFormFields<FollowupFormValues>();
const mutation = useMutation({
...createLeadFollowupMutation,
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'Unable to save lead follow-up');
}
});
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
@@ -42,31 +50,26 @@ export function LeadFollowupPanel({
nextFollowupDate: value.nextFollowupDate || null nextFollowupDate: value.nextFollowupDate || null
} }
}); });
} toast.success('Saved lead follow-up successfully');
});
const mutation = useMutation({
...createLeadFollowupMutation,
onSuccess: () => {
toast.success('บันทึก follow-up สำเร็จ');
form.reset(); form.reset();
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'ไม่สามารถบันทึก follow-up ได้');
} }
}); });
return ( return (
<div className='space-y-4'>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>New Follow-up</CardTitle> <CardTitle>Follow-ups</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className='flex flex-col gap-6'>
<form.AppForm> <form.AppForm>
<form.Form id='lead-followup-form' className='grid gap-4 md:grid-cols-2'> <form.Form id='lead-followup-form' className='grid gap-4 md:grid-cols-2'>
<FormTextField name='followupDate' label='Follow-up Date' placeholder='YYYY-MM-DD' /> <FormDatePickerField
<FormTextField name='followupDate'
label='Follow-up Date'
required
placeholder='YYYY-MM-DD'
/>
<FormDatePickerField
name='nextFollowupDate' name='nextFollowupDate'
label='Next Follow-up Date' label='Next Follow-up Date'
placeholder='YYYY-MM-DD' placeholder='YYYY-MM-DD'
@@ -79,43 +82,46 @@ export function LeadFollowupPanel({
label: item.label label: item.label
}))} }))}
/> />
<div />
<FormTextareaField name='note' label='Note' className='md:col-span-2' />
<div className='md:col-span-2'> <div className='md:col-span-2'>
<FormTextareaField
name='note'
label='Note'
size='md'
placeholder='Summarize the latest customer conversation and next step.'
/>
</div>
</form.Form>
</form.AppForm>
<div className='flex justify-end'>
<Button type='submit' form='lead-followup-form' isLoading={mutation.isPending}> <Button type='submit' form='lead-followup-form' isLoading={mutation.isPending}>
Save Follow-up Save Follow-up
</Button> </Button>
</div> </div>
</form.Form>
</form.AppForm>
</CardContent>
</Card>
<Card> <div className='flex flex-col gap-3'>
<CardHeader>
<CardTitle>History</CardTitle>
</CardHeader>
<CardContent className='space-y-3'>
{detail.followups.length === 0 ? ( {detail.followups.length === 0 ? (
<div className='text-muted-foreground text-sm'>No follow-up history yet.</div> <div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No follow-ups yet.
</div>
) : ( ) : (
detail.followups.map((followup) => ( detail.followups.map((followup) => (
<div key={followup.id} className='border-border rounded-md border p-3'> <div key={followup.id} className='rounded-lg border p-4'>
<div className='flex items-center justify-between gap-3'> <div className='font-medium'>{followup.followupStatusLabel ?? followup.followupStatus}</div>
<div className='font-medium'> <div className='text-muted-foreground mt-1 text-xs'>
{followup.followupStatusLabel ?? followup.followupStatus} {formatDate(followup.followupDate)}
</div> </div>
<div className='text-muted-foreground text-xs'>{formatDate(followup.followupDate)}</div> <div className='mt-2 text-sm'>{followup.note || '-'}</div>
</div> {followup.nextFollowupDate ? (
{followup.note ? <div className='mt-2 text-sm'>{followup.note}</div> : null}
<div className='text-muted-foreground mt-2 text-xs'> <div className='text-muted-foreground mt-2 text-xs'>
{followup.createdByName ?? followup.createdBy} Next: {formatDate(followup.nextFollowupDate)}
</div> </div>
) : null}
</div> </div>
)) ))
)} )}
</div>
</CardContent> </CardContent>
</Card> </Card>
</div>
); );
} }

View File

@@ -258,7 +258,10 @@ async function listLeadActivity(
entityId: log.entityId, entityId: log.entityId,
createdAt: log.createdAt, createdAt: log.createdAt,
userId: log.userId, 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); return buildCrmSecurityContext(input);
} }

View File

@@ -47,15 +47,7 @@ export interface LeadLinkedOpportunitySummary {
updatedAt: string; updatedAt: string;
} }
export interface LeadActivityRecord { export type LeadActivityRecord = CrmAuditLogRecord;
id: string;
action: string;
entityType: string;
entityId: string;
createdAt: string;
userId: string;
actorName: string | null;
}
export interface LeadReferenceData { export interface LeadReferenceData {
awarenesses: LeadOption[]; awarenesses: LeadOption[];
@@ -225,4 +217,5 @@ export interface LeadMutationSuccessResponse {
lead?: LeadDetail; lead?: LeadDetail;
followup?: LeadFollowupSummary; followup?: LeadFollowupSummary;
} }
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';

View File

@@ -166,15 +166,7 @@ export interface OpportunityFollowupRecord {
updatedBy: string; updatedBy: string;
} }
export interface OpportunityActivityRecord { export type OpportunityActivityRecord = CrmAuditLogRecord;
id: string;
action: string;
entityType: string;
entityId: string;
createdAt: string;
userId: string;
actorName: string | null;
}
export interface OpportunityReferenceData { export interface OpportunityReferenceData {
branches: OpportunityBranchOption[]; branches: OpportunityBranchOption[];
@@ -337,3 +329,4 @@ export interface MutationSuccessResponse {
success: boolean; success: boolean;
message: string; message: string;
} }
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';

View File

@@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 { formatDate, formatDateTime } from '@/lib/date-format';
import { formatNumber } from '@/lib/number-format'; import { formatNumber } from '@/lib/number-format';
import type { import type {
@@ -284,7 +285,7 @@ export function OpportunityDetail({
<TabsList> <TabsList>
<TabsTrigger value='followups'>Follow-up Timeline</TabsTrigger> <TabsTrigger value='followups'>Follow-up Timeline</TabsTrigger>
<TabsTrigger value='quotations'>Linked Quotations</TabsTrigger> <TabsTrigger value='quotations'>Linked Quotations</TabsTrigger>
<TabsTrigger value='activity'>Activity</TabsTrigger> <TabsTrigger value='activity'>Audit Log</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='followups'> <TabsContent value='followups'>
@@ -331,26 +332,11 @@ export function OpportunityDetail({
</TabsContent> </TabsContent>
<TabsContent value='activity'> <TabsContent value='activity'>
<Card> <AuditLogTab
<CardHeader> items={data.activity}
<CardTitle>Activity</CardTitle> emptyMessage='No audit log recorded yet.'
</CardHeader> description='System-generated immutable history for this opportunity and its follow-ups.'
<CardContent className='space-y-3'> />
{data.activity.length === 0 ? (
<div className='text-muted-foreground text-sm'>No activity yet.</div>
) : (
data.activity.map((activity) => (
<div key={activity.id} className='border-border rounded-md border p-3'>
<div className='font-medium'>{activity.action}</div>
<div className='text-muted-foreground mt-1 text-xs'>
{activity.actorName ?? activity.userId} -{' '}
{formatDateTime(activity.createdAt)}
</div>
</div>
))
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@@ -1136,7 +1136,10 @@ export async function getOpportunityActivity(
entityId: log.entityId, entityId: log.entityId,
createdAt: log.createdAt, createdAt: log.createdAt,
userId: log.userId, userId: log.userId,
actorName: actorMap.get(log.userId) ?? null actorName: actorMap.get(log.userId) ?? null,
beforeData: log.beforeData,
afterData: log.afterData,
requestId: log.requestId
})); }));
} }

View File

@@ -257,15 +257,7 @@ export interface QuotationAttachmentRecord {
deletedAt: string | null; deletedAt: string | null;
} }
export interface QuotationActivityRecord { export type QuotationActivityRecord = CrmAuditLogRecord;
id: string;
action: string;
entityType: string;
entityId: string;
createdAt: string;
userId: string;
actorName: string | null;
}
export interface QuotationRelationItem { export interface QuotationRelationItem {
id: string; id: string;
@@ -481,3 +473,4 @@ export interface QuotationCustomerPackageGenerateResponse {
pageCount: number; pageCount: number;
} | null; } | null;
} }
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';

View File

@@ -27,6 +27,7 @@ import {
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AuditLogTab } from '@/features/crm/components/audit-log-tab';
import { formatDate, formatDateTime } from '@/lib/date-format'; import { formatDate, formatDateTime } from '@/lib/date-format';
import { formatNumber } from '@/lib/number-format'; import { formatNumber } from '@/lib/number-format';
import { import {
@@ -1231,7 +1232,7 @@ const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
<TabsTrigger value='topics'>Topics</TabsTrigger> <TabsTrigger value='topics'>Topics</TabsTrigger>
<TabsTrigger value='followups'>Follow-ups</TabsTrigger> <TabsTrigger value='followups'>Follow-ups</TabsTrigger>
<TabsTrigger value='attachments'>Attachments</TabsTrigger> <TabsTrigger value='attachments'>Attachments</TabsTrigger>
<TabsTrigger value='activity'>Activity</TabsTrigger> <TabsTrigger value='activity'>Audit Log</TabsTrigger>
<TabsTrigger value='approval'>Approval</TabsTrigger> <TabsTrigger value='approval'>Approval</TabsTrigger>
</TabsList> </TabsList>
@@ -1657,45 +1658,12 @@ const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value='activity'> <TabsContent value='activity'>
<Card> <AuditLogTab
<CardHeader> items={data.activity}
<CardTitle>Activity</CardTitle> emptyMessage='No audit log recorded yet.'
<CardDescription> description='System-generated immutable history for this quotation and its child records.'
Audit events for quotation mutations and child records. />
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{!data.activity.length ? (
<EmptyState message='No activity recorded yet.' />
) : (
data.activity.map((item, index) => (
<div key={item.id} className='space-y-4'>
<div className='flex items-start gap-3'>
<div className='bg-primary mt-2 h-2 w-2 rounded-full' />
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<Badge variant='outline' className='capitalize'>
{item.action}
</Badge>
<Badge variant='secondary'>
{item.entityType.replaceAll('_', ' ')}
</Badge>
<span className='text-sm font-medium'>
{item.actorName ?? item.userId}
</span>
</div>
<div className='text-muted-foreground text-xs'>
{formatDateTime(item.createdAt)}
</div>
</div>
</div>
{index < data.activity.length - 1 ? <Separator /> : null}
</div>
))
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
<TabsContent value='approval'> <TabsContent value='approval'>

View File

@@ -1444,7 +1444,10 @@ export async function getQuotationActivity(
entityId: log.entityId, entityId: log.entityId,
createdAt: log.createdAt, createdAt: log.createdAt,
userId: log.userId, userId: log.userId,
actorName: actorMap.get(log.userId) ?? null actorName: actorMap.get(log.userId) ?? null,
beforeData: log.beforeData,
afterData: log.afterData,
requestId: log.requestId
})); }));
} }

View File

@@ -39,6 +39,12 @@ export const PERMISSIONS = {
crmLeadRead: 'crm.lead.read', crmLeadRead: 'crm.lead.read',
crmLeadCreate: 'crm.lead.create', crmLeadCreate: 'crm.lead.create',
crmLeadUpdate: 'crm.lead.update', 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', crmLeadAssign: 'crm.lead.assign',
crmLeadDelete: 'crm.lead.delete', crmLeadDelete: 'crm.lead.delete',
crmCustomerRead: 'crm.customer.read', crmCustomerRead: 'crm.customer.read',
@@ -513,6 +519,18 @@ PERMISSIONS.crmDocumentTemplateRead,
}; };
export const CRM_PERMISSION_GROUPS: PermissionGroup[] = [ 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', key: 'lead',
label: 'Leads', label: 'Leads',