uat-seed-script
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"audit": "payload-parity",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"previewHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
||||
"generationHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
||||
"snapshotHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"previewHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||
"generationHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||
"snapshotHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||
"mismatches": [],
|
||||
"comparableFieldCount": 31,
|
||||
"topicInputKeyCount": 6,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"overallStatus": "PASS",
|
||||
"results": [
|
||||
{
|
||||
"templateName": "ONVALLA Quotation Standard",
|
||||
"organizationId": "6bf90327-2ce9-4831-93bb-e8d026699ff2",
|
||||
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||
"organizationId": "daf796fc-4588-5240-a27e-9bfef0739658",
|
||||
"version": "1.0",
|
||||
"duplicateFieldNames": [],
|
||||
"typoFields": [],
|
||||
@@ -13,18 +13,6 @@
|
||||
"invalidReservedMappings": [],
|
||||
"missingReservedFields": [],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"templateName": "ALLA Quotation Standard",
|
||||
"organizationId": "d5c46139-9af4-421b-9395-d83620c62f9b",
|
||||
"version": "2.1",
|
||||
"duplicateFieldNames": [],
|
||||
"typoFields": [],
|
||||
"unmappedPlaceholders": [],
|
||||
"deadMappings": [],
|
||||
"invalidReservedMappings": [],
|
||||
"missingReservedFields": [],
|
||||
"status": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,51 +2,51 @@
|
||||
"audit": "runtime-payload",
|
||||
"overallStatus": "PASS",
|
||||
"staticLabelsStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"templateName": "ALLA Quotation Standard",
|
||||
"templateVersion": "2.1",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||
"templateVersion": "1.0",
|
||||
"findingCount": 168,
|
||||
"findings": [
|
||||
{
|
||||
"field": "documentData.company.id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "d5c46139-9af4-421b-9395-d83620c62f9b",
|
||||
"value": "daf796fc-4588-5240-a27e-9bfef0739658",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.company.name",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "ALLA",
|
||||
"value": "ALLA Demo Organization",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.customer.id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "56f1f9a3-6204-4ccc-af0f-5dbf866d5a56",
|
||||
"value": "96954c53-fd37-4d97-80e0-1809756a015d",
|
||||
"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": "Mueang Chiang Mai, Chiang Mai",
|
||||
"value": "Bang Sue, Bangkok",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.customer.phone",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "02-400824",
|
||||
"value": "02-400827",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -60,14 +60,14 @@
|
||||
"field": "documentData.contact.id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "93843e06-65db-492f-9229-066e6dfdec5d",
|
||||
"value": "3fc91684-ebed-49a9-b50e-0fb6fdaae87f",
|
||||
"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": "0810000220",
|
||||
"value": "0810000250",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.contact.position",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Plant Manager",
|
||||
"value": "Procurement Manager",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotation.id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "7dc0630b-e27d-4696-803a-d93f51bb9804",
|
||||
"value": "bb3ebe66-a0ed-4b48-b125-795b549fa798",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotation.code",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "QT2606-1122",
|
||||
"value": "CR2606-1124",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotation.projectName",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Dealer Stock Crane Package",
|
||||
"value": "Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotation.projectLocation",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Chiang Mai Service Warehouse",
|
||||
"value": "Bang Sue Engineering Faculty",
|
||||
"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:dealer-channel-approved",
|
||||
"value": "crm-uat-seed-v1:university-lab-crane-approved",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -151,14 +151,14 @@
|
||||
"field": "documentData.quotation.subtotal",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 4418000,
|
||||
"value": 3008000,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotation.totalAmount",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 4585442,
|
||||
"value": 3122003,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -172,7 +172,7 @@
|
||||
"field": "documentData.items[0].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "7b2a99ab-5106-4ab1-b611-cd63957aa38f",
|
||||
"value": "ab4102fc-7ad9-4338-aee2-b847c10dbf15",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -186,7 +186,7 @@
|
||||
"field": "documentData.items[0].description",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Main equipment supply for Dealer Stock Crane Package",
|
||||
"value": "Main equipment supply for Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -207,7 +207,7 @@
|
||||
"field": "documentData.items[0].unitPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 2521993,
|
||||
"value": 1717102,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -221,14 +221,14 @@
|
||||
"field": "documentData.items[0].totalPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 2521993,
|
||||
"value": 1717102,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.items[1].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "cc058b8e-2ce1-4e66-a72c-6c40474cf962",
|
||||
"value": "9c320ba4-cf4b-4b3b-b72e-c588c8723384",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -242,7 +242,7 @@
|
||||
"field": "documentData.items[1].description",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Installation and commissioning for Dealer Stock Crane Package",
|
||||
"value": "Installation and commissioning for Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -263,7 +263,7 @@
|
||||
"field": "documentData.items[1].unitPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 1283924,
|
||||
"value": 874161,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -277,14 +277,14 @@
|
||||
"field": "documentData.items[1].totalPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 1283924,
|
||||
"value": 874161,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.items[2].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "9436e5e6-6dc7-4fa6-bf8a-da2727992680",
|
||||
"value": "7f64760c-65d6-4b23-9921-4a33d43a2efc",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -298,7 +298,7 @@
|
||||
"field": "documentData.items[2].description",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Operator training and documentation for Dealer Stock Crane Package",
|
||||
"value": "Operator training and documentation for Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -319,7 +319,7 @@
|
||||
"field": "documentData.items[2].unitPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 458544,
|
||||
"value": 312200,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -333,14 +333,14 @@
|
||||
"field": "documentData.items[2].totalPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 458544,
|
||||
"value": 312200,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.items[3].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "63d82897-1660-4f49-b0d1-fddb9712922f",
|
||||
"value": "4b5a4cef-3005-4253-9638-8c3cc9fade15",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -354,7 +354,7 @@
|
||||
"field": "documentData.items[3].description",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Project management and testing for Dealer Stock Crane Package",
|
||||
"value": "Project management and testing for Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -375,7 +375,7 @@
|
||||
"field": "documentData.items[3].unitPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 320981,
|
||||
"value": 218540,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -389,7 +389,7 @@
|
||||
"field": "documentData.items[3].totalPrice",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": 320981,
|
||||
"value": 218540,
|
||||
"message": "Scalar value is populated"
|
||||
},
|
||||
{
|
||||
@@ -403,21 +403,21 @@
|
||||
"field": "documentData.quotationCustomers[0].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "7a20818a-48a1-4eed-a779-ac5ec9d6159b",
|
||||
"value": "04ff8d80-b200-4c01-8081-24d548a2a576",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotationCustomers[0].customerId",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "56f1f9a3-6204-4ccc-af0f-5dbf866d5a56",
|
||||
"value": "96954c53-fd37-4d97-80e0-1809756a015d",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.quotationCustomers[0].role",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "09320e31-e8f9-42cd-a3ea-577210f29501",
|
||||
"value": "a5d93b6c-131b-4e9e-9240-ac94fe12f3d8",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -438,7 +438,7 @@
|
||||
"field": "documentData.topics.all[0].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "ef076be2-fbef-444a-b7cf-cc2523e8b7dc",
|
||||
"value": "58c2d002-1c6f-40c4-b68a-c11ece1fff33",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -473,7 +473,7 @@
|
||||
"field": "documentData.topics.all[0].items[0].content",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Supply and execute dealer stock crane package according to approved drawing.",
|
||||
"value": "Supply and execute material handling lab crane according to approved drawing.",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -501,7 +501,7 @@
|
||||
"field": "documentData.topics.all[1].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "14f41b98-f9d8-463b-9880-8e96d258a6ba",
|
||||
"value": "5a069f2d-dfb1-4ec0-9f51-3f9b657e7731",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -564,7 +564,7 @@
|
||||
"field": "documentData.topics.all[2].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "54500c11-5f51-49d6-947e-db2f4d689959",
|
||||
"value": "d16b5a95-d811-4860-9670-596bef0c13df",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -634,7 +634,7 @@
|
||||
"field": "documentData.topics.exclusion[0].id",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "14f41b98-f9d8-463b-9880-8e96d258a6ba",
|
||||
"value": "5a069f2d-dfb1-4ec0-9f51-3f9b657e7731",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -732,14 +732,14 @@
|
||||
"field": "documentData.pdfme.quotation_date",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Jun 3, 2026",
|
||||
"value": "Jun 15, 2026",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.pdfme.quotation_price",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "THB 4,418,000.00",
|
||||
"value": "THB 3,008,000.00",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -802,49 +802,49 @@
|
||||
"field": "documentData.signatures.preparedBy.name",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "Sales Crane UAT",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.preparedBy.position",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Sales Manager",
|
||||
"value": "Sales Engineer",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.preparedBy.actedAt",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "2026-06-03T09:00:00.000Z",
|
||||
"value": "2026-06-15T09:00:00.000Z",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.approvedBy.name",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "-",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.approvedBy.position",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Sales Manager",
|
||||
"value": "-",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.approvedBy.actedAt",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "2026-06-06T10:00:00.000Z",
|
||||
"message": "String is populated"
|
||||
"status": "WARNING",
|
||||
"value": null,
|
||||
"message": "Value is null"
|
||||
},
|
||||
{
|
||||
"field": "documentData.signatures.authorizedBy.name",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "Sales Manager UAT",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -858,28 +858,28 @@
|
||||
"field": "documentData.signatures.authorizedBy.actedAt",
|
||||
"source": "documentData",
|
||||
"status": "PASS",
|
||||
"value": "2026-06-07T09:00:00.000Z",
|
||||
"value": "2026-06-17T11: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": "Mueang Chiang Mai, Chiang Mai",
|
||||
"value": "Bang Sue, Bangkok",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.customer_tel",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "02-400824",
|
||||
"value": "02-400827",
|
||||
"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": "Dealer Stock Crane Package",
|
||||
"value": "Material Handling Lab Crane",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.site_location",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Chiang Mai Service Warehouse",
|
||||
"value": "Bang Sue Engineering Faculty",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.quotation_date_data",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Jun 3, 2026",
|
||||
"value": "Jun 15, 2026",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.quotation_code_data",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "QT2606-1122",
|
||||
"value": "CR2606-1124",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -935,7 +935,7 @@
|
||||
"field": "templateInput.quotation_price",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "THB 4,418,000.00",
|
||||
"value": "THB 3,008,000.00",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -984,35 +984,35 @@
|
||||
"field": "templateInput.app1",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "Sales Crane UAT",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.app1_position",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Sales Manager",
|
||||
"value": "Sales Engineer",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.app2",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "-",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.app2_position",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Sales Manager",
|
||||
"value": "-",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
"field": "templateInput.app3",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Super Admin",
|
||||
"value": "Sales Manager UAT",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -1026,7 +1026,7 @@
|
||||
"field": "templateInput.quotation_date",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "Jun 3, 2026",
|
||||
"value": "Jun 15, 2026",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -1075,7 +1075,7 @@
|
||||
"field": "templateInput.quotation_code",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "QT2606-1122",
|
||||
"value": "CR2606-1124",
|
||||
"message": "String is populated"
|
||||
},
|
||||
{
|
||||
@@ -1138,7 +1138,7 @@
|
||||
"field": "templateInput.quotation_price_data[0][1]",
|
||||
"source": "templateInput",
|
||||
"status": "PASS",
|
||||
"value": "฿4,418,000.00",
|
||||
"value": "฿3,008,000.00",
|
||||
"message": "Price table amount cell is formatted"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"audit": "approved-snapshot-parity",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"approvedSnapshotAvailable": true,
|
||||
"approvedTemplateVersionId": "86f99414-4656-44a9-9937-8457794dc710",
|
||||
"computedTemplateVersionId": "86f99414-4656-44a9-9937-8457794dc710",
|
||||
"approvedTemplateVersionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||
"computedTemplateVersionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||
"inconsistentTemplateKeys": [],
|
||||
"computedSignatureHash": "72c05979b9a0b0666a06cce5b80dc67fe9c5493eced4366a4cf63785dc415269",
|
||||
"storedSignatureHash": "72c05979b9a0b0666a06cce5b80dc67fe9c5493eced4366a4cf63785dc415269",
|
||||
"approvedArtifactReference": "artifact:ec300b87-3710-43a4-a63c-a72168fe3933",
|
||||
"computedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||
"storedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||
"approvedArtifactReference": null,
|
||||
"refreshedFixtureSnapshot": true,
|
||||
"usedAuditFixture": false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"generatedAt": "2026-06-30T01:35:49.157Z",
|
||||
"generatedAt": "2026-06-30T02:56:03.783Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"artifacts": {
|
||||
"templateVersionReport": "template-version-report.json",
|
||||
"placeholderIntegrityReport": "placeholder-integrity-report.json",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# PDF Integrity Summary
|
||||
|
||||
- Generated At: 2026-06-30T01:35:49.157Z
|
||||
- Generated At: 2026-06-30T02:56:03.783Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: QT2606-1122
|
||||
- Quotation Code: CR2606-1124
|
||||
|
||||
## Statuses
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"audit": "template-version-integrity",
|
||||
"overallStatus": "PASS",
|
||||
"activeTemplateCount": 2,
|
||||
"quotationCode": "QT2606-1122",
|
||||
"activeTemplateCount": 1,
|
||||
"quotationCode": "CR2606-1124",
|
||||
"approvedTemplateVersionId": null,
|
||||
"approvedTemplateVersionIsActive": false,
|
||||
"issues": []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"audit": "topic-runtime",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"topicCount": 3,
|
||||
"topicInputKeys": [
|
||||
"topic_1_0",
|
||||
|
||||
88
docs/audit/package-scripts-cleanup.md
Normal file
88
docs/audit/package-scripts-cleanup.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Package Scripts Cleanup Audit
|
||||
|
||||
## Scope
|
||||
|
||||
- reviewed `package.json`
|
||||
- reviewed `scripts/**`
|
||||
- reviewed `src/db/seeds/**`
|
||||
- reviewed `docs/operations/system-bootstrap.md`
|
||||
- cross-checked historical references in `docs/implementation/**` and setup copy
|
||||
|
||||
## Decision Summary
|
||||
|
||||
| Script Name | Current Command | Status | Reason | Replacement Command |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `dev` | `next dev` | keep | core runtime entrypoint | - |
|
||||
| `build` | `npm run verify:encoding && next build` | keep | build already guards encoding drift | - |
|
||||
| `start` | `next start` | keep | production runtime entrypoint | - |
|
||||
| `lint` | `oxlint` | keep | active lint entrypoint | - |
|
||||
| `lint:fix` | `oxlint --fix && bun format` | keep | active fix workflow | - |
|
||||
| `lint:strict` | `oxlint --deny-warnings` | keep | stricter CI-style lint path | - |
|
||||
| `format` | `oxfmt --write .` | keep | active formatter entrypoint | - |
|
||||
| `format:check` | `oxfmt --check .` | keep | active formatter verification | - |
|
||||
| `prepare` | `husky` | keep | install hook bootstrap | - |
|
||||
| `typecheck` | `tsc --noEmit` | add | required by task and docs verification flow | - |
|
||||
| `gen` | `npm run verify:encoding && npx drizzle-kit generate` | remove | duplicate of `db:generate` with unclear shorthand | `npm run db:generate` |
|
||||
| `db:generate` | `npx drizzle-kit generate` | keep | canonical drizzle generate command | - |
|
||||
| `db:migrate` | `npx drizzle-kit migrate` | keep | canonical drizzle migrate command | - |
|
||||
| `db:studio` | `npx drizzle-kit studio` | keep | canonical drizzle studio command | - |
|
||||
| `db:reset` | `node --experimental-strip-types scripts/db/reset-database.ts` | keep | guarded destructive operation with env safety | - |
|
||||
| `db:setup` | `npm run db:migrate && npm run seed:system` | keep | still referenced by setup flow and valid | - |
|
||||
| `db:setup:uat` | `npm run db:setup && npm run seed:uat` | keep | valid setup composition for UAT | - |
|
||||
| `db:fresh` | `npm run db:reset && npm run db:migrate && npm run seed:system` | keep | clean local bootstrap path | - |
|
||||
| `db:fresh:uat` | `npm run db:fresh && npm run seed:uat` | keep | clean UAT bootstrap path | - |
|
||||
| `migrate` | `npm run db:migrate` | remove | duplicate shorthand | `npm run db:migrate` |
|
||||
| `studio` | `npm run db:studio` | remove | duplicate shorthand | `npm run db:studio` |
|
||||
| `setup:db` | `npm run db:setup` | keep | compatibility alias still referenced by setup wizard copy | `npm run db:setup` |
|
||||
| `seed:super-admin` | `node scripts/seed-super-admin.js` | keep | useful low-level bootstrap step | - |
|
||||
| `seed:foundation` | `node --experimental-strip-types src/db/seeds/foundation.seed.ts` | keep | useful low-level seed for troubleshooting | - |
|
||||
| `seed:crm-uat` | `node --experimental-strip-types src/db/seeds/crm-uat.seed.ts` | keep | useful low-level data-only seed for troubleshooting | - |
|
||||
| `seed:system` | old direct seed file | replace | now routes through `scripts/seed-system.ts` wrapper and includes PDF template reseed | `npm run seed:system` |
|
||||
| `seed:uat` | old direct seed file | replace | now routes through `scripts/seed-uat.ts` wrapper and seeds users before CRM data | `npm run seed:uat` |
|
||||
| `seed:reset` | not present | add | task-required guarded reset + migrate + reseed orchestration | `npm run seed:reset -- --allow-db-reset` |
|
||||
| `seed:pdf-template` | `node --experimental-strip-types scripts/reseed-pdf-template-version.ts` | keep | canonical operator-facing PDF template sync | - |
|
||||
| `seed:pdf-template-version` | same as `seed:pdf-template` | keep | compatibility alias for historical docs and task lineage | `npm run seed:pdf-template` |
|
||||
| `pdf:reseed` | duplicate reseed alias | remove | overlapping alias with no added behavior | `npm run seed:pdf-template` |
|
||||
| `pdf:activate:product` | targeted activation command | keep | still useful for product-v1 operator flow | - |
|
||||
| `migrate:membership-business-roles` | migration utility | keep | one-off but still valid migration helper | - |
|
||||
| `seed:task-h1-fixture` | fixture utility | keep | still referenced by historical PDF verification docs | - |
|
||||
| `legacy:seed:task-h1-fixture` | duplicate legacy alias | remove | duplicate of active fixture command | `npm run seed:task-h1-fixture` |
|
||||
| `verify:task-h1` | fixture verification utility | keep | still referenced by historical docs | - |
|
||||
| `legacy:verify:task-h1` | duplicate legacy alias | remove | duplicate of active verification command | `npm run verify:task-h1` |
|
||||
| `verify:task-h3` | historical verification utility | keep | still valid and referenced | - |
|
||||
| `legacy:verify:task-h3` | duplicate legacy alias | remove | duplicate of active verification command | `npm run verify:task-h3` |
|
||||
| `verify:encoding` | `node scripts/verify-encoding.mjs` | keep | build safety prerequisite | - |
|
||||
| `verify:crm-access` | CRM access audit | keep | task-required verification | - |
|
||||
| `audit:pdf:*` | PDF audit suite | keep | active verification foundation | - |
|
||||
|
||||
## Changes Applied
|
||||
|
||||
- added `typecheck`
|
||||
- added `seed:reset`
|
||||
- moved `seed:system` to `scripts/seed-system.ts`
|
||||
- moved `seed:uat` to `scripts/seed-uat.ts`
|
||||
- kept `seed:pdf-template-version` only as compatibility alias
|
||||
- removed duplicate shorthand and legacy aliases:
|
||||
- `gen`
|
||||
- `migrate`
|
||||
- `studio`
|
||||
- `pdf:reseed`
|
||||
- `legacy:seed:task-h1-fixture`
|
||||
- `legacy:verify:task-h1`
|
||||
- `legacy:verify:task-h3`
|
||||
|
||||
## Seed Architecture Notes
|
||||
|
||||
- `seed:system` now guarantees:
|
||||
- super admin
|
||||
- foundation data
|
||||
- active PDF template reseed
|
||||
- `seed:uat` now guarantees:
|
||||
- deterministic UAT users and memberships
|
||||
- CRM role profiles and role assignments
|
||||
- CRM demo dataset
|
||||
- `seed:reset` now guarantees:
|
||||
- explicit `--allow-db-reset` confirmation or `ALLOW_DB_RESET=true`
|
||||
- database reset safety guard
|
||||
- migrations reapplied before seeding
|
||||
- `seed:system` then `seed:uat`
|
||||
@@ -1,30 +1,61 @@
|
||||
{
|
||||
"generatedAt": "2026-06-30T01:35:48.376Z",
|
||||
"generatedAt": "2026-06-30T02:56:00.835Z",
|
||||
"overallStatus": "PASS",
|
||||
"quotationCode": "QT2606-1122",
|
||||
"quotationCode": "CR2606-1124",
|
||||
"expectedFixtureCode": "QT-H5-AUDIT",
|
||||
"template": {
|
||||
"templateName": "ALLA Quotation Standard",
|
||||
"version": "2.1",
|
||||
"versionId": "86f99414-4656-44a9-9937-8457794dc710",
|
||||
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||
"version": "1.0",
|
||||
"versionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||
"fieldCount": 44,
|
||||
"previousVersion": "2.0",
|
||||
"deletedFields": [
|
||||
"__section_role__customer",
|
||||
"__section_role__product_items",
|
||||
"__section_role__signature",
|
||||
"__section_role__topics",
|
||||
"items_table",
|
||||
"product_items_currency_value",
|
||||
"product_items_footer_note",
|
||||
"product_items_line_168",
|
||||
"product_items_line_35",
|
||||
"product_items_subtitle",
|
||||
"product_items_title",
|
||||
"product_items_total_label",
|
||||
"product_items_total_value"
|
||||
"previousVersion": null,
|
||||
"deletedFields": [],
|
||||
"newFields": [
|
||||
"Please_do_not",
|
||||
"app1",
|
||||
"app1_position",
|
||||
"app2",
|
||||
"app2_position",
|
||||
"app3",
|
||||
"app3_position",
|
||||
"att_label",
|
||||
"colon",
|
||||
"colon copy",
|
||||
"colon_att",
|
||||
"colon_email",
|
||||
"colon_fax",
|
||||
"colon_project",
|
||||
"colon_site",
|
||||
"colon_tel",
|
||||
"company1",
|
||||
"company2",
|
||||
"company_addr1",
|
||||
"company_addr2",
|
||||
"company_email",
|
||||
"company_tax",
|
||||
"company_tel",
|
||||
"customer_addr",
|
||||
"customer_att",
|
||||
"customer_email",
|
||||
"customer_name",
|
||||
"customer_tel",
|
||||
"data_topic",
|
||||
"dear_sirs",
|
||||
"email_label",
|
||||
"line",
|
||||
"project_label",
|
||||
"project_name",
|
||||
"quotation_code_data",
|
||||
"quotation_code_lable",
|
||||
"quotation_date_data",
|
||||
"quotation_date_labe",
|
||||
"quotation_price_data",
|
||||
"site_label",
|
||||
"site_location",
|
||||
"tel_label",
|
||||
"topic",
|
||||
"yours_faithfuly"
|
||||
],
|
||||
"newFields": [],
|
||||
"duplicateFields": [],
|
||||
"unknownFields": [],
|
||||
"classifications": [
|
||||
@@ -232,7 +263,7 @@
|
||||
"consistency": {
|
||||
"status": "PASS",
|
||||
"approvedSnapshotAvailable": true,
|
||||
"approvedArtifactReference": "artifact:ec300b87-3710-43a4-a63c-a72168fe3933",
|
||||
"approvedArtifactReference": null,
|
||||
"inconsistentKeys": []
|
||||
},
|
||||
"integritySummary": {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# PDF Audit Report
|
||||
|
||||
- Generated At: 2026-06-30T01:35:48.376Z
|
||||
- Generated At: 2026-06-30T02:56:00.835Z
|
||||
- Overall Status: PASS
|
||||
- Quotation Code: QT2606-1122
|
||||
- Template: ALLA Quotation Standard v2.1
|
||||
- Previous Version: 2.0
|
||||
- Quotation Code: CR2606-1124
|
||||
- Template: ALLA Demo Organization Quotation Standard v1.0
|
||||
- Previous Version: None
|
||||
|
||||
## Metrics
|
||||
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
|
||||
```bash
|
||||
npm install
|
||||
ALLOW_DB_RESET=true npm run db:fresh
|
||||
npm exec tsc --noEmit
|
||||
npm run db:fresh
|
||||
npm run typecheck
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Fresh UAT or demo data
|
||||
|
||||
```bash
|
||||
ALLOW_DB_RESET=true npm run db:fresh:uat
|
||||
npm run db:fresh:uat
|
||||
```
|
||||
|
||||
## Reset and reseed existing local/UAT data
|
||||
|
||||
```bash
|
||||
npm run seed:reset -- --allow-db-reset
|
||||
```
|
||||
|
||||
## Existing database upgrade
|
||||
@@ -44,7 +50,7 @@ npm run seed:pdf-template
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
npm exec tsc --noEmit
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run verify:encoding
|
||||
npm run verify:crm-access
|
||||
|
||||
86
docs/seeding/uat-seed-guide.md
Normal file
86
docs/seeding/uat-seed-guide.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# UAT Seed Guide
|
||||
|
||||
## Purpose
|
||||
|
||||
Seeded UAT data is designed to make the CRM flow visible end-to-end:
|
||||
|
||||
`Marketing (MK) -> Sales -> Manager -> CEO`
|
||||
|
||||
Default password for all seeded UAT accounts:
|
||||
|
||||
```text
|
||||
UatDemo123!
|
||||
```
|
||||
|
||||
## Seeded Users
|
||||
|
||||
| User Email | Role | What this user should see | Expected CRM data | Expected approval actions |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `mk.uat@alla.local` | Marketing | lead intake, assigned lead queue, follow-up workload, dashboard without final commercial authority | newly created and assigned leads including crane and dock door stories | assign leads to sales, monitor follow-up only |
|
||||
| `sales.crane.uat@alla.local` | Sales - Crane | own crane opportunities and quotations, pricing-visible commercial pipeline for crane work | crane project stories such as Siam Manufacturing / Toyota Gateway Plant | submit crane quotation for approval, track follow-up |
|
||||
| `sales.dockdoor.uat@alla.local` | Sales - Dock Door | own dock door opportunities and quotations, follow-up workload | dock door stories such as Eastern Logistics / WHA Warehouse | submit dock door quotation, maintain follow-up and revisions |
|
||||
| `mgr.sales.uat@alla.local` | Sales Manager + Department Manager | team-wide quotation queue, approval steps 1 and 2, broader sales pipeline | pending manager approval, manager-approved waiting CEO, revised quotations | approve, reject, return, and advance quotations |
|
||||
| `ceo.uat@alla.local` | CEO / Top Manager | executive dashboard, organization-wide quotations, final approval queue | manager-approved quotations waiting final decision, approved deal visibility | final approve or reject quotations |
|
||||
| `admin.uat@alla.local` | CRM Admin | full setup/admin visibility including master options, sequences, approvals, document templates | complete seeded organization data | verify configuration, inspect all data, no special workflow dependency |
|
||||
|
||||
## Seeded Workflow Scenarios
|
||||
|
||||
### Scenario A: Crane Project
|
||||
|
||||
- Customer: `Siam Manufacturing Co., Ltd.`
|
||||
- End Customer: `Toyota Gateway Plant`
|
||||
- Project: `Overhead Crane Installation Phase 1`
|
||||
- Product Type: `Crane`
|
||||
- Owner flow: MK -> Sales Crane -> Sales Manager -> CEO
|
||||
- Expected status: manager-approved and waiting final CEO approval
|
||||
|
||||
### Scenario B: Dock Door Project
|
||||
|
||||
- Customer: `Eastern Logistics Co., Ltd.`
|
||||
- End Customer: `WHA Warehouse`
|
||||
- Project: `Loading Dock Expansion`
|
||||
- Product Type: `Dock Door`
|
||||
- Owner flow: MK -> Sales Dock Door
|
||||
- Expected status: draft/follow-up style commercial progress before final submission
|
||||
|
||||
### Scenario C: Lost / No Quotation
|
||||
|
||||
- Customer: `Demo Lost Customer`
|
||||
- Expected status: `No Quotation`
|
||||
- Expected reason: `Budget not approved`
|
||||
|
||||
### Scenario D: Follow-up Due States
|
||||
|
||||
- seeded at least one follow-up due today
|
||||
- seeded at least one overdue follow-up
|
||||
- seeded at least one upcoming follow-up
|
||||
|
||||
## Approval Flow Expectation
|
||||
|
||||
Seeded workflow: `Quotation Standard Approval`
|
||||
|
||||
1. Sales submits quotation
|
||||
2. Sales Manager approves
|
||||
3. Department Manager approves
|
||||
4. CEO final approves
|
||||
|
||||
For UAT simplicity, `mgr.sales.uat@alla.local` holds both manager stages so the queue is easy to validate visually.
|
||||
|
||||
## Seed Commands
|
||||
|
||||
```bash
|
||||
npm run seed:system
|
||||
npm run seed:uat
|
||||
npm run seed:reset -- --allow-db-reset
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run seed:reset -- --allow-db-reset
|
||||
npm run verify:encoding
|
||||
npm run verify:crm-access
|
||||
npm run audit:pdf
|
||||
```
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
37
package.json
37
package.json
@@ -12,47 +12,42 @@
|
||||
"format": "oxfmt --write .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"prepare": "husky",
|
||||
"gen": "npm run verify:encoding && npx drizzle-kit generate",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"db:generate": "npx drizzle-kit generate",
|
||||
"db:migrate": "npx drizzle-kit migrate",
|
||||
"db:studio": "npx drizzle-kit studio",
|
||||
"db:reset": "node --experimental-strip-types scripts/db/reset-database.ts",
|
||||
"db:reset": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/db/reset-database.ts",
|
||||
"db:setup": "npm run db:migrate && npm run seed:system",
|
||||
"db:setup:uat": "npm run db:setup && npm run seed:uat",
|
||||
"db:fresh": "npm run db:reset && npm run db:migrate && npm run seed:system",
|
||||
"db:fresh:uat": "npm run db:fresh && npm run seed:uat",
|
||||
"migrate": "npm run db:migrate",
|
||||
"studio": "npm run db:studio",
|
||||
"db:fresh": "npm run db:reset -- --allow-db-reset && npm run db:migrate && npm run seed:system",
|
||||
"db:fresh:uat": "npm run db:reset -- --allow-db-reset && npm run db:migrate && npm run seed:system && npm run seed:uat",
|
||||
"setup:db": "npm run db:setup",
|
||||
"seed:super-admin": "node scripts/seed-super-admin.js",
|
||||
"seed:foundation": "node --experimental-strip-types src/db/seeds/foundation.seed.ts",
|
||||
"seed:crm-uat": "node --experimental-strip-types src/db/seeds/crm-uat.seed.ts",
|
||||
"seed:pdf-template-version": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
||||
"seed:system": "node --experimental-strip-types src/db/seeds/system.seed.ts",
|
||||
"seed:uat": "node --experimental-strip-types src/db/seeds/uat.seed.ts",
|
||||
"seed:pdf-template": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
||||
"seed:foundation": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types src/db/seeds/foundation.seed.ts",
|
||||
"seed:crm-uat": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types src/db/seeds/crm-uat.seed.ts",
|
||||
"seed:system": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-system.ts",
|
||||
"seed:uat": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-uat.ts",
|
||||
"seed:reset": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-reset.ts",
|
||||
"seed:pdf-template": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
||||
"seed:pdf-template-version": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
||||
"migrate:membership-business-roles": "node --experimental-strip-types scripts/migrate-membership-business-roles.ts",
|
||||
"seed:task-h1-fixture": "node scripts/seed-task-h1-fixture.js",
|
||||
"legacy:seed:task-h1-fixture": "node scripts/seed-task-h1-fixture.js",
|
||||
"verify:task-h1": "node scripts/verify-task-h1.js",
|
||||
"verify:task-h3": "node --experimental-strip-types scripts/verify-task-h3.js",
|
||||
"legacy:verify:task-h1": "node scripts/verify-task-h1.js",
|
||||
"legacy:verify:task-h3": "node --experimental-strip-types scripts/verify-task-h3.js",
|
||||
"verify:task-h3": "node scripts/verify-task-h3.js",
|
||||
"verify:crm-access": "node --experimental-strip-types scripts/security/verify-crm-access.ts",
|
||||
"verify:encoding": "node scripts/verify-encoding.mjs",
|
||||
"audit:pdf:inventory": "node --experimental-strip-types scripts/audit-pdf-template-inventory.ts",
|
||||
"audit:pdf:coverage": "node --experimental-strip-types scripts/audit-pdf-mapping-coverage.ts",
|
||||
"audit:pdf:runtime": "node --experimental-strip-types scripts/audit-pdf-runtime-payload.ts",
|
||||
"audit:pdf:report": "node --experimental-strip-types scripts/generate-pdf-audit-report.ts",
|
||||
"verify:encoding": "node scripts/verify-encoding.mjs",
|
||||
"audit:pdf:versions": "node --experimental-strip-types scripts/audit-template-version-integrity.ts",
|
||||
"audit:pdf:placeholders": "node --experimental-strip-types scripts/audit-placeholder-integrity.ts",
|
||||
"audit:pdf:parity": "node --experimental-strip-types scripts/audit-payload-parity.ts",
|
||||
"audit:pdf:snapshot": "node --experimental-strip-types scripts/audit-approved-snapshot-parity.ts",
|
||||
"audit:pdf:integrity": "node --experimental-strip-types scripts/generate-pdf-integrity-report.ts",
|
||||
"audit:pdf:visual": "npx tsx scripts/audit-pdf-visual-regression.ts",
|
||||
"audit:pdf:visual:baseline": "npx tsx scripts/audit-pdf-visual-regression.ts -- --write-baseline",
|
||||
"audit:pdf:visual": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts",
|
||||
"audit:pdf:visual:baseline": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts --refresh-baseline",
|
||||
"audit:pdf": "npm run audit:pdf:inventory && npm run audit:pdf:coverage && npm run audit:pdf:runtime && npm run audit:pdf:versions && npm run audit:pdf:placeholders && npm run audit:pdf:parity && npm run audit:pdf:snapshot && npm run audit:pdf:report && npm run audit:pdf:integrity",
|
||||
"setup:db": "npm run db:setup",
|
||||
"pdf:reseed": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
||||
"pdf:activate:product": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts --template-variant=product-v1 --activate"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
433
plans/task-clean-pk.md
Normal file
433
plans/task-clean-pk.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Task: Clean package.json Scripts + UAT Seed + Resettable System Seed
|
||||
|
||||
## Role
|
||||
|
||||
You are a Next.js full-stack engineer working on ALLA OS CRM.
|
||||
|
||||
Tech stack:
|
||||
|
||||
* Next.js Full Stack
|
||||
* TypeScript
|
||||
* Drizzle ORM
|
||||
* PostgreSQL
|
||||
* shadcn/ui
|
||||
* Auth.js / Keycloak-compatible user model
|
||||
* pdfme document template system
|
||||
|
||||
## Objective
|
||||
|
||||
Clean unused `package.json` scripts and implement deterministic seed scripts for UAT/demo data that clearly shows the CRM workflow:
|
||||
|
||||
```text
|
||||
Marketing (MK) -> Sales -> Manager -> CEO
|
||||
```
|
||||
|
||||
The system must be resettable to a clean starting state at any time.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Audit and Clean package.json
|
||||
|
||||
Inspect `package.json`, `scripts/`, `src/db/seeds/`, `drizzle/`, and existing documentation.
|
||||
|
||||
### Requirements
|
||||
|
||||
1. Identify scripts that are:
|
||||
|
||||
* broken
|
||||
* duplicated
|
||||
* unused
|
||||
* pointing to missing files
|
||||
* legacy from old architecture
|
||||
* confusing or overlapping
|
||||
|
||||
2. Do not delete blindly.
|
||||
|
||||
* First create a report file:
|
||||
|
||||
```text
|
||||
docs/audit/package-scripts-cleanup.md
|
||||
```
|
||||
|
||||
3. The report must include:
|
||||
|
||||
```text
|
||||
Script Name
|
||||
Current Command
|
||||
Status: keep / rename / remove / replace
|
||||
Reason
|
||||
Replacement Command if any
|
||||
```
|
||||
|
||||
4. Then update `package.json`.
|
||||
|
||||
### Recommended final script groups
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "...",
|
||||
"build": "...",
|
||||
"start": "...",
|
||||
"lint": "...",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
"db:generate": "...",
|
||||
"db:migrate": "...",
|
||||
"db:studio": "...",
|
||||
"db:fresh": "...",
|
||||
"db:fresh:uat": "...",
|
||||
|
||||
"seed:system": "...",
|
||||
"seed:uat": "...",
|
||||
"seed:reset": "...",
|
||||
"seed:pdf-template": "...",
|
||||
|
||||
"verify:encoding": "...",
|
||||
"verify:crm-access": "...",
|
||||
"audit:pdf": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the actual project commands after inspection.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Implement Resettable Seed Architecture
|
||||
|
||||
Create or normalize seed scripts under:
|
||||
|
||||
```text
|
||||
src/db/seeds/
|
||||
scripts/
|
||||
```
|
||||
|
||||
Expected command behavior:
|
||||
|
||||
### 1. System seed
|
||||
|
||||
```bash
|
||||
npm run seed:system
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
* create minimum required system data
|
||||
* organizations
|
||||
* branches
|
||||
* product types
|
||||
* roles
|
||||
* permissions
|
||||
* master options
|
||||
* document sequences
|
||||
* approval definitions
|
||||
* PDF template records
|
||||
|
||||
Must be idempotent.
|
||||
|
||||
---
|
||||
|
||||
### 2. UAT seed
|
||||
|
||||
```bash
|
||||
npm run seed:uat
|
||||
```
|
||||
|
||||
Purpose:
|
||||
Create realistic demo data for UAT so each role can log in and clearly see its own view.
|
||||
|
||||
Seed role flow:
|
||||
|
||||
```text
|
||||
MK User
|
||||
creates Lead
|
||||
|
||||
Sales User
|
||||
receives assigned Lead
|
||||
works Opportunity
|
||||
creates Quotation
|
||||
|
||||
Manager User
|
||||
sees team pipeline
|
||||
approves quotation step 1 or 2
|
||||
|
||||
CEO User
|
||||
sees executive dashboard
|
||||
final approval
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Full reset seed
|
||||
|
||||
```bash
|
||||
ALLOW_DB_RESET=true npm run seed:reset
|
||||
```
|
||||
|
||||
Purpose:
|
||||
Reset all business/UAT data and rebuild from clean baseline.
|
||||
|
||||
Rules:
|
||||
|
||||
* Must require `ALLOW_DB_RESET=true`
|
||||
* Must refuse to run without this env flag
|
||||
* Must never run accidentally in production
|
||||
* Must clear CRM transactional tables in dependency-safe order
|
||||
* Must preserve migrations
|
||||
* Must rebuild system seed
|
||||
* Must rebuild UAT seed
|
||||
|
||||
Expected flow:
|
||||
|
||||
```text
|
||||
validate environment
|
||||
clear transactional CRM data
|
||||
clear configurable seed-owned master data if needed
|
||||
run seed:system
|
||||
run seed:uat
|
||||
run audit/verification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: UAT Demo Users
|
||||
|
||||
Create clear users and memberships.
|
||||
|
||||
Example users:
|
||||
|
||||
```text
|
||||
mk.uat@alla.local
|
||||
sales.crane.uat@alla.local
|
||||
sales.dockdoor.uat@alla.local
|
||||
mgr.sales.uat@alla.local
|
||||
ceo.uat@alla.local
|
||||
admin.uat@alla.local
|
||||
```
|
||||
|
||||
Each user must have:
|
||||
|
||||
* deterministic id
|
||||
* name
|
||||
* email
|
||||
* role
|
||||
* businessRole
|
||||
* permissions
|
||||
* organization membership
|
||||
* branch scope
|
||||
* product type scope where applicable
|
||||
|
||||
Use existing schema and permission model. Do not invent unrelated tables.
|
||||
|
||||
---
|
||||
|
||||
## Part 4: UAT Demo Data Scenario
|
||||
|
||||
Create data that makes the dashboard and permissions obvious.
|
||||
|
||||
### Scenario A: Crane Project
|
||||
|
||||
```text
|
||||
Customer: Siam Manufacturing Co., Ltd.
|
||||
End Customer: Toyota Gateway Plant
|
||||
Project: Overhead Crane Installation Phase 1
|
||||
Product Type: Crane
|
||||
Lead created by: MK
|
||||
Assigned to: Sales Crane
|
||||
Manager: Sales Manager
|
||||
Final approver: CEO
|
||||
Quotation status: Pending Approval / Approved
|
||||
Value: realistic THB amount
|
||||
```
|
||||
|
||||
### Scenario B: Dock Door Project
|
||||
|
||||
```text
|
||||
Customer: Eastern Logistics Co., Ltd.
|
||||
End Customer: WHA Warehouse
|
||||
Project: Loading Dock Expansion
|
||||
Product Type: Dock Door
|
||||
Lead created by: MK
|
||||
Assigned to: Sales Dock Door
|
||||
Quotation status: Draft / Follow-up
|
||||
```
|
||||
|
||||
### Scenario C: Lost / No Quotation
|
||||
|
||||
```text
|
||||
Customer: Demo Lost Customer
|
||||
Reason: Budget not approved / competitor selected
|
||||
Status: Closed Lost or No Quotation
|
||||
```
|
||||
|
||||
### Scenario D: Follow-up Due
|
||||
|
||||
Create at least:
|
||||
|
||||
* one due today
|
||||
* one overdue
|
||||
* one upcoming
|
||||
|
||||
This helps test calendar/dashboard/follow-up UI.
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Approval Flow Seed
|
||||
|
||||
Create approval definition:
|
||||
|
||||
```text
|
||||
Quotation Standard Approval
|
||||
Step 1: Sales Manager
|
||||
Step 2: Department Manager or Business Manager
|
||||
Step 3: CEO
|
||||
```
|
||||
|
||||
For UAT, make the flow visually simple:
|
||||
|
||||
```text
|
||||
Sales submits quotation
|
||||
Manager approves
|
||||
CEO final approves
|
||||
```
|
||||
|
||||
Seed at least:
|
||||
|
||||
* one draft quotation
|
||||
* one submitted quotation
|
||||
* one manager-approved quotation waiting for CEO
|
||||
* one fully approved quotation
|
||||
* one rejected quotation if supported
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Document Sequence Seed
|
||||
|
||||
Seed document sequences for all main document types:
|
||||
|
||||
```text
|
||||
Lead
|
||||
Opportunity
|
||||
Quotation
|
||||
PO if supported
|
||||
```
|
||||
|
||||
Code format:
|
||||
|
||||
```text
|
||||
ProductType + YYMM + running
|
||||
CR2606-001
|
||||
DK2606-001
|
||||
SOL2606-001
|
||||
SV2606-001
|
||||
SP2606-001
|
||||
```
|
||||
|
||||
Branch should represent internal ALLA branch/business unit, not customer branch.
|
||||
|
||||
---
|
||||
|
||||
## Part 7: PDF Template Seed
|
||||
|
||||
Ensure PDF template seeding is included.
|
||||
|
||||
Runtime must use DB template versions, not raw JSON directly.
|
||||
|
||||
Add or confirm command:
|
||||
|
||||
```bash
|
||||
npm run seed:pdf-template
|
||||
```
|
||||
|
||||
If template reset is needed, add:
|
||||
|
||||
```bash
|
||||
npm run seed:pdf-template:reset
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
||||
* deactivate old active template versions if needed
|
||||
* insert/update template version from source JSON
|
||||
* activate the correct version
|
||||
* run `npm run audit:pdf`
|
||||
|
||||
Do not leave multiple active versions for the same template.
|
||||
|
||||
---
|
||||
|
||||
## Part 8: Safety Requirements
|
||||
|
||||
1. All seed scripts must be idempotent.
|
||||
2. Running `npm run seed:uat` twice must not duplicate data.
|
||||
3. Reset must require explicit env flag.
|
||||
4. Scripts must log what they changed.
|
||||
5. Use deterministic IDs or stable unique keys.
|
||||
6. Do not hide errors with empty catch blocks.
|
||||
7. Use transactions where practical.
|
||||
8. Add verification output after seed.
|
||||
|
||||
---
|
||||
|
||||
## Part 9: Verification Commands
|
||||
|
||||
After implementation, run:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run seed:reset
|
||||
npm run verify:encoding
|
||||
npm run verify:crm-access
|
||||
npm run audit:pdf
|
||||
```
|
||||
|
||||
If some commands do not exist, either:
|
||||
|
||||
* add them properly, or
|
||||
* document why they are not available.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
Create or update:
|
||||
|
||||
```text
|
||||
package.json
|
||||
docs/audit/package-scripts-cleanup.md
|
||||
docs/seeding/uat-seed-guide.md
|
||||
scripts/seed-system.ts
|
||||
scripts/seed-uat.ts
|
||||
scripts/seed-reset.ts
|
||||
scripts/reseed-pdf-template-version.ts if needed
|
||||
src/db/seeds/*
|
||||
```
|
||||
|
||||
The UAT seed guide must include:
|
||||
|
||||
```text
|
||||
User
|
||||
Email
|
||||
Role
|
||||
What this user should see
|
||||
Expected CRM data
|
||||
Expected approval actions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* `package.json` has no dead scripts.
|
||||
* `npm run seed:system` works.
|
||||
* `npm run seed:uat` works.
|
||||
* `ALLOW_DB_RESET=true npm run seed:reset` resets and reseeds cleanly.
|
||||
* UAT data clearly demonstrates MK -> Sales -> Manager -> CEO.
|
||||
* Each role sees different data according to permission.
|
||||
* Quotation approval flow has testable documents.
|
||||
* PDF template audit passes.
|
||||
* No duplicate active PDF template version exists.
|
||||
* TypeScript passes.
|
||||
* Build passes.
|
||||
@@ -42,11 +42,21 @@ function loadLocalEnv() {
|
||||
loadEnvFile(path.resolve(process.cwd(), '.env'));
|
||||
}
|
||||
|
||||
const ALLOW_DB_RESET_FLAG = '--allow-db-reset';
|
||||
|
||||
function assertSafeToReset(databaseUrl: string) {
|
||||
if (process.env.ALLOW_DB_RESET !== 'true') {
|
||||
throw new Error('ALLOW_DB_RESET=true is required.');
|
||||
const allowDbReset =
|
||||
process.env.ALLOW_DB_RESET === 'true' ||
|
||||
process.argv.includes(ALLOW_DB_RESET_FLAG);
|
||||
|
||||
if (!allowDbReset) {
|
||||
throw new Error(
|
||||
`Database reset requires explicit confirmation. Run "npm run db:reset -- ${ALLOW_DB_RESET_FLAG}".`
|
||||
);
|
||||
}
|
||||
|
||||
process.env.ALLOW_DB_RESET = 'true';
|
||||
|
||||
if ((process.env.NODE_ENV ?? '').toLowerCase() === 'production') {
|
||||
throw new Error('Database reset is blocked in NODE_ENV=production.');
|
||||
}
|
||||
|
||||
110
scripts/lib/seed-utils.ts
Normal file
110
scripts/lib/seed-utils.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
|
||||
export type SqlClient = ReturnType<typeof postgres>;
|
||||
|
||||
function loadEnvFile(filePath: string) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = line.indexOf('=');
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
let value = line.slice(separatorIndex + 1).trim();
|
||||
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
if (!(key in process.env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadLocalEnv() {
|
||||
loadEnvFile(path.resolve(process.cwd(), '.env.local'));
|
||||
loadEnvFile(path.resolve(process.cwd(), '.env'));
|
||||
}
|
||||
|
||||
export function requireEnv(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createSqlClient(): SqlClient {
|
||||
loadLocalEnv();
|
||||
return postgres(requireEnv('DATABASE_URL'), { prepare: false });
|
||||
}
|
||||
|
||||
export function deterministicId(input: string): string {
|
||||
const digest = createHash('sha256').update(input).digest('hex');
|
||||
const part3 = `5${digest.slice(13, 16)}`;
|
||||
const part4 = `a${digest.slice(17, 20)}`;
|
||||
|
||||
return [
|
||||
digest.slice(0, 8),
|
||||
digest.slice(8, 12),
|
||||
part3,
|
||||
part4,
|
||||
digest.slice(20, 32)
|
||||
].join('-');
|
||||
}
|
||||
|
||||
export const NODE_TS_RUNTIME_ARGS = [
|
||||
'--disable-warning=ExperimentalWarning',
|
||||
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||
'--experimental-strip-types'
|
||||
];
|
||||
|
||||
export function runNodeScript(label: string, scriptPath: string, args: string[] = []) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[...NODE_TS_RUNTIME_ARGS, scriptPath, ...args],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: process.env
|
||||
}
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[${label}] failed with exit code ${result.status ?? 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function runCommand(label: string, command: string, args: string[] = []) {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[${label}] failed with exit code ${result.status ?? 1}`);
|
||||
}
|
||||
}
|
||||
25
scripts/seed-reset.ts
Normal file
25
scripts/seed-reset.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { runCommand, runNodeScript } from './lib/seed-utils.ts';
|
||||
|
||||
const ALLOW_DB_RESET_FLAG = '--allow-db-reset';
|
||||
|
||||
function main() {
|
||||
const allowDbReset =
|
||||
process.env.ALLOW_DB_RESET === 'true' ||
|
||||
process.argv.includes(ALLOW_DB_RESET_FLAG);
|
||||
|
||||
if (!allowDbReset) {
|
||||
throw new Error(
|
||||
`seed:reset requires explicit confirmation. Run "npm run seed:reset -- ${ALLOW_DB_RESET_FLAG}".`
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize the guard so downstream reset scripts keep the same safety contract.
|
||||
process.env.ALLOW_DB_RESET = 'true';
|
||||
|
||||
runNodeScript('seed:reset:db-reset', 'scripts/db/reset-database.ts');
|
||||
runCommand('seed:reset:db-migrate', 'npx', ['drizzle-kit', 'migrate']);
|
||||
runNodeScript('seed:reset:seed-system', 'scripts/seed-system.ts');
|
||||
runNodeScript('seed:reset:seed-uat', 'scripts/seed-uat.ts');
|
||||
}
|
||||
|
||||
main();
|
||||
7
scripts/seed-system.ts
Normal file
7
scripts/seed-system.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { runNodeScript } from './lib/seed-utils.ts';
|
||||
|
||||
function main() {
|
||||
runNodeScript('seed:system:core', 'src/db/seeds/system.seed.ts');
|
||||
}
|
||||
|
||||
main();
|
||||
7
scripts/seed-uat.ts
Normal file
7
scripts/seed-uat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { runNodeScript } from './lib/seed-utils.ts';
|
||||
|
||||
function main() {
|
||||
runNodeScript('seed:uat:core', 'src/db/seeds/uat.seed.ts');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,5 +1,9 @@
|
||||
'use client';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
"use client";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -7,8 +11,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -22,18 +26,18 @@ import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail
|
||||
} from '@/components/ui/sidebar';
|
||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
||||
import { navGroups } from '@/config/nav-config';
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { useFilteredNavGroups } from '@/hooks/use-nav';
|
||||
import Link from 'next/link';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { Icons } from '../icons';
|
||||
import { OrgSwitcher } from '../org-switcher';
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||
import { navGroups } from "@/config/nav-config";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useFilteredNavGroups } from "@/hooks/use-nav";
|
||||
import Link from "next/link";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { Icons } from "../icons";
|
||||
import { OrgSwitcher } from "../org-switcher";
|
||||
|
||||
export default function AppSidebar() {
|
||||
const pathname = usePathname();
|
||||
@@ -44,59 +48,67 @@ export default function AppSidebar() {
|
||||
const user = session?.user ?? null;
|
||||
const organization = session?.user?.activeOrganizationId;
|
||||
const canManageOrganization =
|
||||
session?.user?.systemRole === 'super_admin' ||
|
||||
(!!organization && session?.user?.activeMembershipRole === 'admin');
|
||||
session?.user?.systemRole === "super_admin" ||
|
||||
(!!organization && session?.user?.activeMembershipRole === "admin");
|
||||
|
||||
React.useEffect(() => {
|
||||
// Side effects based on sidebar state changes
|
||||
}, [isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('[sidebar-debug]', {
|
||||
console.info("[sidebar-debug]", {
|
||||
systemRole: session?.user?.systemRole ?? null,
|
||||
activeOrganizationId: session?.user?.activeOrganizationId ?? null,
|
||||
activeMembershipRole: session?.user?.activeMembershipRole ?? null,
|
||||
activePermissions: session?.user?.activePermissions ?? [],
|
||||
visibleItems: filteredGroups.flatMap((group) => group.items.map((item) => item.title))
|
||||
visibleItems: filteredGroups.flatMap((group) =>
|
||||
group.items.map((item) => item.title),
|
||||
),
|
||||
});
|
||||
}, [
|
||||
filteredGroups,
|
||||
session?.user?.activeOrganizationId,
|
||||
session?.user?.activePermissions,
|
||||
session?.user?.activeMembershipRole,
|
||||
session?.user?.systemRole
|
||||
session?.user?.systemRole,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Sidebar collapsible='icon'>
|
||||
<SidebarHeader className='group-data-[collapsible=icon]:pt-4'>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="group-data-[collapsible=icon]:pt-4">
|
||||
<OrgSwitcher />
|
||||
</SidebarHeader>
|
||||
<SidebarContent className='overflow-x-hidden'>
|
||||
<SidebarContent className="overflow-x-hidden">
|
||||
{filteredGroups.map((group) => (
|
||||
<SidebarGroup key={group.label || 'ungrouped'} className='py-0'>
|
||||
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
||||
<SidebarGroup key={group.label || "ungrouped"} className="py-0">
|
||||
{group.label && (
|
||||
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
||||
)}
|
||||
<SidebarMenu>
|
||||
{group.items.map((item) => {
|
||||
const Icon = item.icon ? Icons[item.icon] : Icons.logo;
|
||||
const itemKey = item.url || `${group.label ?? 'group'}-${item.title}`;
|
||||
const itemKey =
|
||||
item.url || `${group.label ?? "group"}-${item.title}`;
|
||||
return item?.items && item?.items?.length > 0 ? (
|
||||
<Collapsible
|
||||
key={itemKey}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className='group/collapsible'
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title} isActive={pathname === item.url}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={pathname === item.url}
|
||||
>
|
||||
{item.icon && <Icon />}
|
||||
<span>{item.title}</span>
|
||||
<Icons.chevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
||||
<Icons.chevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
@@ -105,7 +117,10 @@ export default function AppSidebar() {
|
||||
<SidebarMenuSubItem
|
||||
key={subItem.url || `${itemKey}-${subItem.title}`}
|
||||
>
|
||||
<SidebarMenuSubButton asChild isActive={pathname === subItem.url}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={pathname === subItem.url}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
@@ -141,49 +156,57 @@ export default function AppSidebar() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
{user && (
|
||||
<UserAvatarProfile className='h-8 w-8 rounded-lg' showInfo user={user} />
|
||||
<UserAvatarProfile
|
||||
className="h-8 w-8 rounded-lg"
|
||||
showInfo
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<Icons.chevronsDown className='ml-auto size-4' />
|
||||
<Icons.chevronsDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
||||
side='bottom'
|
||||
align='end'
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='p-0 font-normal'>
|
||||
<div className='px-1 py-1.5'>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="px-1 py-1.5">
|
||||
{user && (
|
||||
<UserAvatarProfile className='h-8 w-8 rounded-lg' showInfo user={user} />
|
||||
<UserAvatarProfile
|
||||
className="h-8 w-8 rounded-lg"
|
||||
showInfo
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
||||
<Icons.account className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/dashboard/profile")}
|
||||
>
|
||||
<Icons.account className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
{canManageOrganization && (
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/billing')}>
|
||||
<Icons.creditCard className='mr-2 h-4 w-4' />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/notifications')}>
|
||||
<Icons.notification className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/dashboard/notifications")}
|
||||
>
|
||||
<Icons.notification className="mr-2 h-4 w-4" />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
||||
<Icons.logout className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||
>
|
||||
<Icons.logout className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
export function UserNav() {
|
||||
const { data: session } = useSession();
|
||||
const user = session?.user;
|
||||
@@ -21,28 +21,37 @@ export function UserNav() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<UserAvatarProfile user={user} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='w-56' align='end' sideOffset={10} forceMount>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<p className='text-sm leading-none font-medium'>{user.name}</p>
|
||||
<p className='text-muted-foreground text-xs leading-none'>{user.email}</p>
|
||||
<DropdownMenuContent
|
||||
className="w-56"
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
forceMount
|
||||
>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">{user.name}</p>
|
||||
<p className="text-muted-foreground text-xs leading-none">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
||||
<DropdownMenuItem onClick={() => router.push("/dashboard/profile")}>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>Billing</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||
>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -345,6 +345,7 @@ type UserContext = {
|
||||
admin: UserMembershipRow;
|
||||
marketing: UserMembershipRow;
|
||||
sales: UserMembershipRow[];
|
||||
salesByProductType: Partial<Record<string, UserMembershipRow>>;
|
||||
salesManager: UserMembershipRow;
|
||||
departmentManager: UserMembershipRow;
|
||||
topManager: UserMembershipRow;
|
||||
@@ -352,33 +353,41 @@ type UserContext = {
|
||||
|
||||
const SEED_TAG = "crm-uat-seed-v1";
|
||||
const PERIOD = "2606";
|
||||
const UAT_EMAILS = {
|
||||
marketing: "mk.uat@alla.local",
|
||||
salesCrane: "sales.crane.uat@alla.local",
|
||||
salesDockDoor: "sales.dockdoor.uat@alla.local",
|
||||
salesManager: "mgr.sales.uat@alla.local",
|
||||
topManager: "ceo.uat@alla.local",
|
||||
admin: "admin.uat@alla.local",
|
||||
} as const;
|
||||
|
||||
const STORIES: StoryConfig[] = [
|
||||
{
|
||||
slug: "website-warehouse-crane-win",
|
||||
title: "Lead received from website and won after approval",
|
||||
title: "Siam Manufacturing crane project waiting final CEO approval",
|
||||
customerName: "บริษัท บางกอก ฟู้ด อินดัสทรี จำกัด",
|
||||
province: "Bangkok",
|
||||
district: "Min Buri",
|
||||
province: "Chachoengsao",
|
||||
district: "Bang Pakong",
|
||||
branchCode: "head_office",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "factory",
|
||||
productTypeCode: "crane",
|
||||
quotationTypeCode: "crane",
|
||||
priorityCode: "high",
|
||||
leadChannelCode: "website",
|
||||
leadChannelCode: "referral",
|
||||
awarenessCode: "google_search_website",
|
||||
leadStatusCode: "assigned",
|
||||
leadFollowupStatusCode: "waiting_po",
|
||||
leadFollowupStatusCode: "drawing_followup",
|
||||
opportunityStatusCode: "negotiation",
|
||||
chancePercent: 80,
|
||||
estimatedValue: 12400000,
|
||||
projectName: "Warehouse Crane Installation Phase 2",
|
||||
projectLocation: "Bangkok Food Industrial Estate, Min Buri",
|
||||
chancePercent: 78,
|
||||
estimatedValue: 8750000,
|
||||
projectName: "Overhead Crane Installation Phase 1",
|
||||
projectLocation: "Toyota Gateway Plant",
|
||||
leadDescription:
|
||||
"Customer asked for top-running overhead crane to support raw material warehouse expansion.",
|
||||
quotePlan: "accepted",
|
||||
opportunityOutcome: "won",
|
||||
"Marketing qualified the crane project and handed it to the crane sales queue for quotation approval.",
|
||||
quotePlan: "pending_step_2",
|
||||
opportunityOutcome: "open",
|
||||
multiParties: [
|
||||
{
|
||||
roleCode: "contractor",
|
||||
@@ -392,57 +401,57 @@ const STORIES: StoryConfig[] = [
|
||||
},
|
||||
{
|
||||
slug: "facebook-lost-price",
|
||||
title: "Lead lost to competitor because price too high",
|
||||
title: "Demo lost customer with no quotation because budget not approved",
|
||||
customerName: "บริษัท ไทย ออโต้ เพรส จำกัด",
|
||||
province: "Samut Prakan",
|
||||
district: "Bang Phli",
|
||||
branchCode: "factory",
|
||||
province: "Bangkok",
|
||||
district: "Lat Krabang",
|
||||
branchCode: "service",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "factory",
|
||||
productTypeCode: "crane",
|
||||
quotationTypeCode: "crane",
|
||||
customerSubGroupCode: "warehouse",
|
||||
productTypeCode: "dockdoor",
|
||||
quotationTypeCode: "dockdoor",
|
||||
priorityCode: "normal",
|
||||
leadChannelCode: "facebook",
|
||||
awarenessCode: "social_media",
|
||||
leadStatusCode: "closed_lost",
|
||||
leadFollowupStatusCode: "quotation_preparation",
|
||||
opportunityStatusCode: "quotation_created",
|
||||
chancePercent: 45,
|
||||
estimatedValue: 5800000,
|
||||
projectName: "Stamping Line Hoist Replacement",
|
||||
projectLocation: "Bang Phli Industrial Estate",
|
||||
leadChannelCode: "website",
|
||||
awarenessCode: "google_search_website",
|
||||
leadStatusCode: "cancel",
|
||||
leadFollowupStatusCode: "budget_pending",
|
||||
opportunityStatusCode: "qualification",
|
||||
chancePercent: 20,
|
||||
estimatedValue: 1900000,
|
||||
projectName: "Budget Hold Demo Project",
|
||||
projectLocation: "Lat Krabang Logistics Zone",
|
||||
leadDescription:
|
||||
"Customer compared 5 ton monorail hoist replacement between ALLA and Konecranes.",
|
||||
competitor: "Konecranes",
|
||||
quotePlan: "sent_follow_up",
|
||||
opportunityOutcome: "lost",
|
||||
lostReasonCode: "price_too_high",
|
||||
"Opportunity was stopped before quotation because customer budget was not approved and competitor was still under consideration.",
|
||||
competitor: "Competitor Selected",
|
||||
quotePlan: "cancelled",
|
||||
opportunityOutcome: "no_quotation",
|
||||
noQuotationReasonCode: "budget_not_ready",
|
||||
},
|
||||
{
|
||||
slug: "revision-r02-approved",
|
||||
title: "Customer requested revision and approved revision R02",
|
||||
title: "Eastern Logistics dock door project in draft and follow-up stage",
|
||||
customerName: "บริษัท กรีน โลจิสติกส์ พาร์ค จำกัด",
|
||||
province: "Pathum Thani",
|
||||
district: "Khlong Luang",
|
||||
province: "Chonburi",
|
||||
district: "Si Racha",
|
||||
branchCode: "service",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "logistics",
|
||||
customerSubGroupCode: "warehouse",
|
||||
productTypeCode: "dockdoor",
|
||||
quotationTypeCode: "dockdoor",
|
||||
priorityCode: "high",
|
||||
leadChannelCode: "referral",
|
||||
awarenessCode: "word_of_mouth",
|
||||
leadStatusCode: "assigned",
|
||||
leadFollowupStatusCode: "waiting_po",
|
||||
opportunityStatusCode: "negotiation",
|
||||
chancePercent: 70,
|
||||
estimatedValue: 9300000,
|
||||
projectName: "Cold Chain Dock Leveller Upgrade",
|
||||
projectLocation: "Khlong Luang Logistics Campus",
|
||||
leadFollowupStatusCode: "quotation_preparation",
|
||||
opportunityStatusCode: "proposal_preparation",
|
||||
chancePercent: 61,
|
||||
estimatedValue: 4600000,
|
||||
projectName: "Loading Dock Expansion",
|
||||
projectLocation: "WHA Warehouse",
|
||||
leadDescription:
|
||||
"Customer requested revision after consultant changed dock layout and bumper specification.",
|
||||
quotePlan: "rejected_then_revised",
|
||||
opportunityOutcome: "won",
|
||||
"Dock door salesperson is still following up layout confirmation before final quotation submission.",
|
||||
quotePlan: "sent_follow_up",
|
||||
opportunityOutcome: "open",
|
||||
multiParties: [
|
||||
{
|
||||
roleCode: "consultant",
|
||||
@@ -882,6 +891,30 @@ function buildDocumentCode(prefix: string, number: number) {
|
||||
return `${prefix}${PERIOD}-${String(number).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function buildQuotationCode(productTypeCode: string, number: number) {
|
||||
const prefixByProductType: Record<string, string> = {
|
||||
crane: "CR",
|
||||
dockdoor: "DK",
|
||||
solarcell: "SOL",
|
||||
service: "SV",
|
||||
spare_part: "SP",
|
||||
};
|
||||
|
||||
return buildDocumentCode(prefixByProductType[productTypeCode] ?? "QT", number);
|
||||
}
|
||||
|
||||
function pickSalesUser(
|
||||
users: UserContext,
|
||||
productTypeCode: string | undefined,
|
||||
index: number
|
||||
) {
|
||||
if (productTypeCode && users.salesByProductType[productTypeCode]) {
|
||||
return users.salesByProductType[productTypeCode]!;
|
||||
}
|
||||
|
||||
return choose(users.sales, index);
|
||||
}
|
||||
|
||||
function seededNote(scope: string) {
|
||||
return `[${SEED_TAG}:${scope}]`;
|
||||
}
|
||||
@@ -1027,21 +1060,48 @@ function buildUserContext(rows: UserMembershipRow[]): UserContext {
|
||||
}
|
||||
|
||||
const first = rows[0];
|
||||
const byRole = (role: string) =>
|
||||
rows.find((row) => row.businessRole === role) ?? null;
|
||||
const salesRows = rows.filter((row) => row.businessRole === "sales");
|
||||
const byRole = (role: string) => rows.find((row) => row.businessRole === role) ?? null;
|
||||
const byEmail = (email: string) =>
|
||||
rows.find((row) => row.email.toLowerCase() === email.toLowerCase()) ?? null;
|
||||
const fallbackSalesRows = rows.filter((row) => row.businessRole === "sales");
|
||||
const preferredSalesRows = [
|
||||
byEmail(UAT_EMAILS.salesCrane),
|
||||
byEmail(UAT_EMAILS.salesDockDoor),
|
||||
].filter((row): row is UserMembershipRow => row !== null);
|
||||
const salesRows = preferredSalesRows.length
|
||||
? preferredSalesRows
|
||||
: fallbackSalesRows.length
|
||||
? fallbackSalesRows
|
||||
: [rows.find((row) => row.membershipRole === "admin") ?? first];
|
||||
const admin =
|
||||
byEmail(UAT_EMAILS.admin) ??
|
||||
rows.find((row) => row.membershipRole === "admin") ??
|
||||
rows.find((row) => row.systemRole === "super_admin") ??
|
||||
first;
|
||||
|
||||
return {
|
||||
admin,
|
||||
marketing: byRole("sales_support") ?? admin,
|
||||
sales: salesRows.length ? salesRows : [admin],
|
||||
salesManager: byRole("sales_manager") ?? admin,
|
||||
departmentManager: byRole("department_manager") ?? admin,
|
||||
topManager: byRole("top_manager") ?? admin,
|
||||
marketing:
|
||||
byEmail(UAT_EMAILS.marketing) ??
|
||||
byRole("marketing") ??
|
||||
byRole("sales_support") ??
|
||||
admin,
|
||||
sales: salesRows,
|
||||
salesByProductType: {
|
||||
crane: byEmail(UAT_EMAILS.salesCrane) ?? preferredSalesRows[0] ?? salesRows[0],
|
||||
dockdoor:
|
||||
byEmail(UAT_EMAILS.salesDockDoor) ??
|
||||
preferredSalesRows.at(-1) ??
|
||||
salesRows.at(-1) ??
|
||||
salesRows[0],
|
||||
},
|
||||
salesManager: byEmail(UAT_EMAILS.salesManager) ?? byRole("sales_manager") ?? admin,
|
||||
departmentManager:
|
||||
byEmail(UAT_EMAILS.salesManager) ??
|
||||
byRole("department_manager") ??
|
||||
byRole("sales_manager") ??
|
||||
admin,
|
||||
topManager: byEmail(UAT_EMAILS.topManager) ?? byRole("top_manager") ?? admin,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1395,7 +1455,7 @@ function buildLeadAndOpportunityData(
|
||||
}
|
||||
const contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
||||
const createdAt = daysAgo(88 - storyIndex * 4);
|
||||
const assignedUser = choose(users.sales, storyIndex);
|
||||
const assignedUser = pickSalesUser(users, story.productTypeCode, storyIndex);
|
||||
const assignedAt = addDays(createdAt, 4);
|
||||
const statusId = getOptionId(
|
||||
optionMap,
|
||||
@@ -2004,7 +2064,7 @@ function buildQuotationData(
|
||||
const customer = storyCustomerMap.get(story.slug);
|
||||
if (!opportunity || !customer) continue;
|
||||
const contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
||||
const salesperson = choose(users.sales, storyIndex);
|
||||
const salesperson = pickSalesUser(users, story.productTypeCode, storyIndex);
|
||||
const baseDate = addDays(opportunity.createdAt, 10);
|
||||
const planRevisionCount =
|
||||
story.quotePlan === "returned_then_revised"
|
||||
@@ -2067,7 +2127,7 @@ function buildQuotationData(
|
||||
|
||||
quotations.push({
|
||||
id: quotationId,
|
||||
code: buildDocumentCode("QT", quotationSequence++),
|
||||
code: buildQuotationCode(story.productTypeCode, quotationSequence++),
|
||||
opportunityId: opportunity.id,
|
||||
customerId: customer.id,
|
||||
contactId: contact?.id ?? null,
|
||||
@@ -2405,7 +2465,10 @@ function buildQuotationData(
|
||||
const quotationId = crypto.randomUUID();
|
||||
quotations.push({
|
||||
id: quotationId,
|
||||
code: buildDocumentCode("QT", quotationSequence++),
|
||||
code: buildQuotationCode(
|
||||
choose(["crane", "dockdoor", "solarcell"], quotations.length),
|
||||
quotationSequence++
|
||||
),
|
||||
opportunityId: null,
|
||||
customerId: customer.id,
|
||||
contactId: contact?.id ?? null,
|
||||
|
||||
129
src/db/seeds/system-organization.seed.ts
Normal file
129
src/db/seeds/system-organization.seed.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { getDefaultPermissions } from '../../lib/auth/rbac.ts';
|
||||
import { createSqlClient, deterministicId, type SqlClient } from '../../../scripts/lib/seed-utils.ts';
|
||||
|
||||
type UserRow = {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const DEFAULT_ORGANIZATION = {
|
||||
id: deterministicId('organization:alla-demo'),
|
||||
name: 'ALLA Demo Organization',
|
||||
slug: 'alla-demo',
|
||||
membershipId: deterministicId('membership:alla-demo:super-admin')
|
||||
};
|
||||
|
||||
async function resolveSuperAdmin(sql: SqlClient): Promise<UserRow> {
|
||||
const email = process.env.SUPER_ADMIN_EMAIL?.trim().toLowerCase();
|
||||
|
||||
if (!email) {
|
||||
throw new Error('SUPER_ADMIN_EMAIL is required before running system organization seed.');
|
||||
}
|
||||
|
||||
const [user] = await sql<UserRow[]>`
|
||||
select id, email
|
||||
from users
|
||||
where email = ${email}
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Super admin user not found. Run seed-super-admin first.');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function ensureOrganization(sql: SqlClient, superAdmin: UserRow) {
|
||||
await sql`
|
||||
insert into organizations (
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
plan,
|
||||
created_by
|
||||
) values (
|
||||
${DEFAULT_ORGANIZATION.id},
|
||||
${DEFAULT_ORGANIZATION.name},
|
||||
${DEFAULT_ORGANIZATION.slug},
|
||||
${'enterprise'},
|
||||
${superAdmin.id}
|
||||
)
|
||||
on conflict (slug) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
plan = excluded.plan,
|
||||
updated_at = now()
|
||||
`;
|
||||
|
||||
const membershipPermissions = getDefaultPermissions('admin', 'crm_admin');
|
||||
const [existingMembership] = await sql<{ id: string }[]>`
|
||||
select id
|
||||
from memberships
|
||||
where user_id = ${superAdmin.id}
|
||||
and organization_id = ${DEFAULT_ORGANIZATION.id}
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (existingMembership) {
|
||||
await sql`
|
||||
update memberships
|
||||
set
|
||||
role = ${'admin'},
|
||||
business_role = ${'crm_admin'},
|
||||
permissions = ${sql.array(membershipPermissions)},
|
||||
updated_at = now()
|
||||
where id = ${existingMembership.id}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into memberships (
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
business_role,
|
||||
permissions,
|
||||
branch_scope_ids,
|
||||
product_type_scope_ids
|
||||
) values (
|
||||
${DEFAULT_ORGANIZATION.membershipId},
|
||||
${superAdmin.id},
|
||||
${DEFAULT_ORGANIZATION.id},
|
||||
${'admin'},
|
||||
${'crm_admin'},
|
||||
${sql.array(membershipPermissions)},
|
||||
${sql.array([])},
|
||||
${sql.array([])}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await sql`
|
||||
update users
|
||||
set
|
||||
active_organization_id = ${DEFAULT_ORGANIZATION.id},
|
||||
updated_at = now()
|
||||
where id = ${superAdmin.id}
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sql = createSqlClient();
|
||||
|
||||
try {
|
||||
const superAdmin = await resolveSuperAdmin(sql);
|
||||
await ensureOrganization(sql, superAdmin);
|
||||
console.log(`[seed:system-org] Ready ${DEFAULT_ORGANIZATION.name}`);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
'[seed:system-org] Failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const NODE_TS_RUNTIME_ARGS = [
|
||||
'--disable-warning=ExperimentalWarning',
|
||||
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||
'--experimental-strip-types'
|
||||
];
|
||||
|
||||
function run(label: string, args: string[]) {
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
stdio: 'inherit',
|
||||
@@ -13,7 +19,12 @@ function run(label: string, args: string[]) {
|
||||
|
||||
function main() {
|
||||
run('super admin seed', ['scripts/seed-super-admin.js']);
|
||||
run('foundation seed', ['--experimental-strip-types', 'src/db/seeds/foundation.seed.ts']);
|
||||
run('system organization seed', [
|
||||
...NODE_TS_RUNTIME_ARGS,
|
||||
'src/db/seeds/system-organization.seed.ts'
|
||||
]);
|
||||
run('foundation seed', [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/foundation.seed.ts']);
|
||||
run('pdf template seed', [...NODE_TS_RUNTIME_ARGS, 'scripts/reseed-pdf-template-version.ts']);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
431
src/db/seeds/uat-users.seed.ts
Normal file
431
src/db/seeds/uat-users.seed.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { getDefaultPermissions, getDefaultRoleProfiles } from '../../lib/auth/rbac.ts';
|
||||
import { createSqlClient, deterministicId, type SqlClient } from '../../../scripts/lib/seed-utils.ts';
|
||||
|
||||
type OrganizationRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
};
|
||||
|
||||
type OptionRow = {
|
||||
id: string;
|
||||
category: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type RoleProfileRow = {
|
||||
id: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type UatRoleAssignmentSeed = {
|
||||
roleCode: string;
|
||||
isPrimary: boolean;
|
||||
branchScopeMode: 'all' | 'selected' | 'inherit';
|
||||
branchCodes?: string[];
|
||||
productTypeScopeMode: 'all' | 'selected' | 'inherit';
|
||||
productTypeCodes?: string[];
|
||||
};
|
||||
|
||||
type UatUserSeed = {
|
||||
key: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
systemRole: 'super_admin' | 'user';
|
||||
membershipRole: 'admin' | 'user';
|
||||
businessRole:
|
||||
| 'marketing'
|
||||
| 'sales'
|
||||
| 'sales_support'
|
||||
| 'sales_manager'
|
||||
| 'department_manager'
|
||||
| 'top_manager'
|
||||
| 'crm_admin';
|
||||
roleAssignments: UatRoleAssignmentSeed[];
|
||||
};
|
||||
|
||||
const UAT_PASSWORD = 'UatDemo123!';
|
||||
|
||||
const UAT_USERS: UatUserSeed[] = [
|
||||
{
|
||||
key: 'mk',
|
||||
name: 'MK UAT',
|
||||
email: 'mk.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'marketing',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'marketing',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'sales-crane',
|
||||
name: 'Sales Crane UAT',
|
||||
email: 'sales.crane.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'selected',
|
||||
productTypeCodes: ['crane']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'sales-dockdoor',
|
||||
name: 'Sales Dock Door UAT',
|
||||
email: 'sales.dockdoor.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'selected',
|
||||
productTypeCodes: ['dockdoor']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'mgr-sales',
|
||||
name: 'Sales Manager UAT',
|
||||
email: 'mgr.sales.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales_manager',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales_manager',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
},
|
||||
{
|
||||
roleCode: 'department_manager',
|
||||
isPrimary: false,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'ceo',
|
||||
name: 'CEO UAT',
|
||||
email: 'ceo.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'top_manager',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'top_manager',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
name: 'Admin UAT',
|
||||
email: 'admin.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'admin',
|
||||
businessRole: 'crm_admin',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'crm_admin',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
async function ensureRoleProfiles(
|
||||
sql: SqlClient,
|
||||
organization: OrganizationRow
|
||||
): Promise<Map<string, RoleProfileRow>> {
|
||||
const defaultProfiles = getDefaultRoleProfiles();
|
||||
|
||||
for (const profile of defaultProfiles) {
|
||||
await sql`
|
||||
insert into crm_role_profiles (
|
||||
id,
|
||||
organization_id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
branch_scope_mode,
|
||||
product_scope_mode,
|
||||
ownership_scope,
|
||||
approval_authority,
|
||||
is_system,
|
||||
is_active,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${deterministicId(`crm-role-profile:${organization.id}:${profile.code}`)},
|
||||
${organization.id},
|
||||
${profile.code},
|
||||
${profile.name},
|
||||
${profile.description},
|
||||
${sql.array(profile.permissions)},
|
||||
${profile.branchScopeMode},
|
||||
${profile.productScopeMode},
|
||||
${profile.ownershipScope},
|
||||
${profile.approvalAuthority},
|
||||
${profile.isSystem},
|
||||
${true},
|
||||
${organization.createdBy},
|
||||
${organization.createdBy}
|
||||
)
|
||||
on conflict (organization_id, code) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
permissions = excluded.permissions,
|
||||
branch_scope_mode = excluded.branch_scope_mode,
|
||||
product_scope_mode = excluded.product_scope_mode,
|
||||
ownership_scope = excluded.ownership_scope,
|
||||
approval_authority = excluded.approval_authority,
|
||||
is_system = excluded.is_system,
|
||||
is_active = excluded.is_active,
|
||||
updated_by = excluded.updated_by,
|
||||
deleted_at = null,
|
||||
updated_at = now()
|
||||
`;
|
||||
}
|
||||
|
||||
const rows = await sql<RoleProfileRow[]>`
|
||||
select id, code
|
||||
from crm_role_profiles
|
||||
where organization_id = ${organization.id}
|
||||
and deleted_at is null
|
||||
`;
|
||||
|
||||
return new Map(rows.map((row) => [row.code, row]));
|
||||
}
|
||||
|
||||
async function loadOptions(sql: SqlClient, organizationId: string): Promise<Map<string, OptionRow>> {
|
||||
const rows = await sql<OptionRow[]>`
|
||||
select id, category, code
|
||||
from ms_options
|
||||
where organization_id = ${organizationId}
|
||||
and deleted_at is null
|
||||
and is_active = true
|
||||
`;
|
||||
|
||||
return new Map(rows.map((row) => [`${row.category}:${row.code}`, row]));
|
||||
}
|
||||
|
||||
function resolveScopeIds(
|
||||
optionMap: Map<string, OptionRow>,
|
||||
category: string,
|
||||
codes: string[] | undefined
|
||||
): string[] {
|
||||
if (!codes?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return codes.map((code) => {
|
||||
const row = optionMap.get(`${category}:${code}`);
|
||||
|
||||
if (!row) {
|
||||
throw new Error(`Missing ${category} option for code ${code}`);
|
||||
}
|
||||
|
||||
return row.id;
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertUatUser(
|
||||
sql: SqlClient,
|
||||
organization: OrganizationRow,
|
||||
optionMap: Map<string, OptionRow>,
|
||||
roleProfileMap: Map<string, RoleProfileRow>,
|
||||
seed: UatUserSeed
|
||||
) {
|
||||
const passwordHash = await bcrypt.hash(seed.password, 10);
|
||||
const [userRow] = await sql<{ id: string }[]>`
|
||||
insert into users (
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
system_role,
|
||||
active_organization_id
|
||||
) values (
|
||||
${deterministicId(`uat-user:${seed.email}`)},
|
||||
${seed.name},
|
||||
${seed.email},
|
||||
${passwordHash},
|
||||
${seed.systemRole},
|
||||
${organization.id}
|
||||
)
|
||||
on conflict (email) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
password_hash = excluded.password_hash,
|
||||
system_role = excluded.system_role,
|
||||
active_organization_id = excluded.active_organization_id,
|
||||
updated_at = now()
|
||||
returning id
|
||||
`;
|
||||
|
||||
const membershipPermissions = getDefaultPermissions(seed.membershipRole, seed.businessRole);
|
||||
const existingMembership =
|
||||
(
|
||||
await sql<{ id: string }[]>`
|
||||
select id
|
||||
from memberships
|
||||
where user_id = ${userRow.id}
|
||||
and organization_id = ${organization.id}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
|
||||
if (existingMembership) {
|
||||
await sql`
|
||||
update memberships
|
||||
set
|
||||
role = ${seed.membershipRole},
|
||||
business_role = ${seed.businessRole},
|
||||
permissions = ${sql.array(membershipPermissions)},
|
||||
branch_scope_ids = ${sql.array([])},
|
||||
product_type_scope_ids = ${sql.array([])},
|
||||
updated_at = now()
|
||||
where id = ${existingMembership.id}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into memberships (
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
business_role,
|
||||
permissions,
|
||||
branch_scope_ids,
|
||||
product_type_scope_ids
|
||||
) values (
|
||||
${deterministicId(`membership:${organization.id}:${seed.email}`)},
|
||||
${userRow.id},
|
||||
${organization.id},
|
||||
${seed.membershipRole},
|
||||
${seed.businessRole},
|
||||
${sql.array(membershipPermissions)},
|
||||
${sql.array([])},
|
||||
${sql.array([])}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await sql`
|
||||
delete from crm_user_role_assignments
|
||||
where organization_id = ${organization.id}
|
||||
and user_id = ${userRow.id}
|
||||
`;
|
||||
|
||||
for (const assignment of seed.roleAssignments) {
|
||||
const roleProfile = roleProfileMap.get(assignment.roleCode);
|
||||
|
||||
if (!roleProfile) {
|
||||
throw new Error(`Missing CRM role profile ${assignment.roleCode}`);
|
||||
}
|
||||
|
||||
await sql`
|
||||
insert into crm_user_role_assignments (
|
||||
id,
|
||||
organization_id,
|
||||
user_id,
|
||||
role_profile_id,
|
||||
branch_scope_mode,
|
||||
branch_scope_ids,
|
||||
product_type_scope_mode,
|
||||
product_type_scope_ids,
|
||||
is_primary,
|
||||
is_active,
|
||||
assigned_by,
|
||||
assigned_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
) values (
|
||||
${deterministicId(`crm-role-assignment:${organization.id}:${seed.email}:${assignment.roleCode}`)},
|
||||
${organization.id},
|
||||
${userRow.id},
|
||||
${roleProfile.id},
|
||||
${assignment.branchScopeMode},
|
||||
${sql.array(resolveScopeIds(optionMap, 'crm_branch', assignment.branchCodes))},
|
||||
${assignment.productTypeScopeMode},
|
||||
${sql.array(resolveScopeIds(optionMap, 'crm_product_type', assignment.productTypeCodes))},
|
||||
${assignment.isPrimary},
|
||||
${true},
|
||||
${organization.createdBy},
|
||||
${new Date()},
|
||||
${new Date()},
|
||||
${new Date()},
|
||||
${null}
|
||||
)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function seedOrganization(sql: SqlClient, organization: OrganizationRow) {
|
||||
const optionMap = await loadOptions(sql, organization.id);
|
||||
const roleProfileMap = await ensureRoleProfiles(sql, organization);
|
||||
|
||||
for (const seed of UAT_USERS) {
|
||||
await upsertUatUser(sql, organization, optionMap, roleProfileMap, seed);
|
||||
}
|
||||
|
||||
console.log(`[seed:uat-users] ${organization.name}: ${UAT_USERS.length} UAT users ready`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sql = createSqlClient();
|
||||
|
||||
try {
|
||||
const organizations = await sql<OrganizationRow[]>`
|
||||
select id, name, created_by as "createdBy"
|
||||
from organizations
|
||||
order by name asc
|
||||
`;
|
||||
|
||||
if (organizations.length === 0) {
|
||||
throw new Error('No organizations found. Run seed:system first.');
|
||||
}
|
||||
|
||||
for (const organization of organizations) {
|
||||
await seedOrganization(sql, organization);
|
||||
}
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[seed:uat-users] Failed:', error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,17 +1,32 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const NODE_TS_RUNTIME_ARGS = [
|
||||
'--disable-warning=ExperimentalWarning',
|
||||
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||
'--experimental-strip-types'
|
||||
];
|
||||
|
||||
function main() {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-strip-types', 'src/db/seeds/crm-uat.seed.ts'],
|
||||
const steps: Array<{ label: string; args: string[] }> = [
|
||||
{
|
||||
label: 'uat users seed',
|
||||
args: [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/uat-users.seed.ts']
|
||||
},
|
||||
{
|
||||
label: 'crm uat data seed',
|
||||
args: [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/crm-uat.seed.ts']
|
||||
}
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
const result = spawnSync(process.execPath, step.args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[seed:uat] failed with exit code ${result.status ?? 1}`);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[seed:uat] ${step.label} failed with exit code ${result.status ?? 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const signInSchema = z.object({
|
||||
|
||||
export default function SignInViewPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard/crm";
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const form = useAppForm({
|
||||
|
||||
@@ -610,7 +610,7 @@ async function canAccessCustomerRow(
|
||||
accessContext,
|
||||
));
|
||||
|
||||
if (accessContext.ownershipScope === "monitor") {
|
||||
if (accessContext.ownershipScope === 'monitor') {
|
||||
return opportunityCustomerIds.has(row.id);
|
||||
}
|
||||
|
||||
|
||||
44
src/proxy.ts
44
src/proxy.ts
@@ -1,29 +1,39 @@
|
||||
import { auth } from '@/auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from "@/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const protectedApiPrefixes = ['/api/products', '/api/organizations', '/api/users'];
|
||||
const superAdminOnlyRoutes = ['/dashboard/workspaces'];
|
||||
const organizationAdminRoutes = ['/dashboard/users', '/dashboard/workspaces/team'];
|
||||
const protectedApiPrefixes = [
|
||||
"/api/products",
|
||||
"/api/organizations",
|
||||
"/api/users",
|
||||
];
|
||||
const superAdminOnlyRoutes = ["/dashboard/workspaces"];
|
||||
const organizationAdminRoutes = [
|
||||
"/dashboard/users",
|
||||
"/dashboard/workspaces/team",
|
||||
];
|
||||
|
||||
export default auth((req) => {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const isDashboardRoute = pathname.startsWith('/dashboard');
|
||||
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) => pathname.startsWith(prefix));
|
||||
const isDashboardRoute = pathname.startsWith("/dashboard/crm");
|
||||
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) =>
|
||||
pathname.startsWith(prefix),
|
||||
);
|
||||
const user = req.auth?.user;
|
||||
const isSignedIn = !!user;
|
||||
const isSuperAdmin = user?.systemRole === 'super_admin';
|
||||
const isSuperAdmin = user?.systemRole === "super_admin";
|
||||
const isOrganizationAdmin =
|
||||
isSuperAdmin ||
|
||||
(user?.activeOrganizationId &&
|
||||
(user.activeMembershipRole === 'admin' || user.activePermissions?.includes('users:manage')));
|
||||
(user.activeMembershipRole === "admin" ||
|
||||
user.activePermissions?.includes("users:manage")));
|
||||
|
||||
if (!isSignedIn && (isDashboardRoute || isProtectedApiRoute)) {
|
||||
if (isProtectedApiRoute) {
|
||||
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const signInUrl = new URL('/auth/sign-in', req.nextUrl.origin);
|
||||
signInUrl.searchParams.set('callbackUrl', pathname);
|
||||
const signInUrl = new URL("/auth/sign-in", req.nextUrl.origin);
|
||||
signInUrl.searchParams.set("callbackUrl", pathname);
|
||||
return NextResponse.redirect(signInUrl);
|
||||
}
|
||||
|
||||
@@ -32,7 +42,7 @@ export default auth((req) => {
|
||||
superAdminOnlyRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||
!isSuperAdmin
|
||||
) {
|
||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
||||
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -40,7 +50,7 @@ export default auth((req) => {
|
||||
organizationAdminRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||
!isOrganizationAdmin
|
||||
) {
|
||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
||||
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
@@ -48,7 +58,7 @@ export default auth((req) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||
'/(api|trpc)(.*)'
|
||||
]
|
||||
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||
"/(api|trpc)(.*)",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user