uat-seed-script
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"audit": "payload-parity",
|
"audit": "payload-parity",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"previewHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
"previewHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||||
"generationHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
"generationHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||||
"snapshotHash": "c3cc553fc210355e7f54726b414e7030be0a64604f6a175499e0b3787aef31a1",
|
"snapshotHash": "a55689ba304d5b0b2b9f6d94ca943b21233eb790de8f9b631a0ecb03ffa699d7",
|
||||||
"mismatches": [],
|
"mismatches": [],
|
||||||
"comparableFieldCount": 31,
|
"comparableFieldCount": 31,
|
||||||
"topicInputKeyCount": 6,
|
"topicInputKeyCount": 6,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"templateName": "ONVALLA Quotation Standard",
|
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||||
"organizationId": "6bf90327-2ce9-4831-93bb-e8d026699ff2",
|
"organizationId": "daf796fc-4588-5240-a27e-9bfef0739658",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"duplicateFieldNames": [],
|
"duplicateFieldNames": [],
|
||||||
"typoFields": [],
|
"typoFields": [],
|
||||||
@@ -13,18 +13,6 @@
|
|||||||
"invalidReservedMappings": [],
|
"invalidReservedMappings": [],
|
||||||
"missingReservedFields": [],
|
"missingReservedFields": [],
|
||||||
"status": "PASS"
|
"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",
|
"audit": "runtime-payload",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"staticLabelsStatus": "PASS",
|
"staticLabelsStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"templateName": "ALLA Quotation Standard",
|
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||||
"templateVersion": "2.1",
|
"templateVersion": "1.0",
|
||||||
"findingCount": 168,
|
"findingCount": 168,
|
||||||
"findings": [
|
"findings": [
|
||||||
{
|
{
|
||||||
"field": "documentData.company.id",
|
"field": "documentData.company.id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "d5c46139-9af4-421b-9395-d83620c62f9b",
|
"value": "daf796fc-4588-5240-a27e-9bfef0739658",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.company.name",
|
"field": "documentData.company.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "ALLA",
|
"value": "ALLA Demo Organization",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.customer.id",
|
"field": "documentData.customer.id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "56f1f9a3-6204-4ccc-af0f-5dbf866d5a56",
|
"value": "96954c53-fd37-4d97-80e0-1809756a015d",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.customer.name",
|
"field": "documentData.customer.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด",
|
"value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.customer.address",
|
"field": "documentData.customer.address",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Mueang Chiang Mai, Chiang Mai",
|
"value": "Bang Sue, Bangkok",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.customer.phone",
|
"field": "documentData.customer.phone",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "02-400824",
|
"value": "02-400827",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,14 +60,14 @@
|
|||||||
"field": "documentData.contact.id",
|
"field": "documentData.contact.id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "93843e06-65db-492f-9229-066e6dfdec5d",
|
"value": "3fc91684-ebed-49a9-b50e-0fb6fdaae87f",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.contact.name",
|
"field": "documentData.contact.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "คุณกิตติพงศ์ สุวรรณ",
|
"value": "คุณวราภรณ์ เกษมศรี",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,56 +81,56 @@
|
|||||||
"field": "documentData.contact.mobile",
|
"field": "documentData.contact.mobile",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "0810000220",
|
"value": "0810000250",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.contact.position",
|
"field": "documentData.contact.position",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Plant Manager",
|
"value": "Procurement Manager",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.id",
|
"field": "documentData.quotation.id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "7dc0630b-e27d-4696-803a-d93f51bb9804",
|
"value": "bb3ebe66-a0ed-4b48-b125-795b549fa798",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.code",
|
"field": "documentData.quotation.code",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "QT2606-1122",
|
"value": "CR2606-1124",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.projectName",
|
"field": "documentData.quotation.projectName",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Dealer Stock Crane Package",
|
"value": "Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.projectLocation",
|
"field": "documentData.quotation.projectLocation",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Chiang Mai Service Warehouse",
|
"value": "Bang Sue Engineering Faculty",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.attention",
|
"field": "documentData.quotation.attention",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "คุณกิตติพงศ์ สุวรรณ",
|
"value": "คุณวราภรณ์ เกษมศรี",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.reference",
|
"field": "documentData.quotation.reference",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "crm-uat-seed-v1:dealer-channel-approved",
|
"value": "crm-uat-seed-v1:university-lab-crane-approved",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,14 +151,14 @@
|
|||||||
"field": "documentData.quotation.subtotal",
|
"field": "documentData.quotation.subtotal",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 4418000,
|
"value": 3008000,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotation.totalAmount",
|
"field": "documentData.quotation.totalAmount",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 4585442,
|
"value": 3122003,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
"field": "documentData.items[0].id",
|
"field": "documentData.items[0].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "7b2a99ab-5106-4ab1-b611-cd63957aa38f",
|
"value": "ab4102fc-7ad9-4338-aee2-b847c10dbf15",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
"field": "documentData.items[0].description",
|
"field": "documentData.items[0].description",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Main equipment supply for Dealer Stock Crane Package",
|
"value": "Main equipment supply for Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
"field": "documentData.items[0].unitPrice",
|
"field": "documentData.items[0].unitPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 2521993,
|
"value": 1717102,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -221,14 +221,14 @@
|
|||||||
"field": "documentData.items[0].totalPrice",
|
"field": "documentData.items[0].totalPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 2521993,
|
"value": 1717102,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.items[1].id",
|
"field": "documentData.items[1].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "cc058b8e-2ce1-4e66-a72c-6c40474cf962",
|
"value": "9c320ba4-cf4b-4b3b-b72e-c588c8723384",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"field": "documentData.items[1].description",
|
"field": "documentData.items[1].description",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Installation and commissioning for Dealer Stock Crane Package",
|
"value": "Installation and commissioning for Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
"field": "documentData.items[1].unitPrice",
|
"field": "documentData.items[1].unitPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 1283924,
|
"value": 874161,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -277,14 +277,14 @@
|
|||||||
"field": "documentData.items[1].totalPrice",
|
"field": "documentData.items[1].totalPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 1283924,
|
"value": 874161,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.items[2].id",
|
"field": "documentData.items[2].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "9436e5e6-6dc7-4fa6-bf8a-da2727992680",
|
"value": "7f64760c-65d6-4b23-9921-4a33d43a2efc",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
"field": "documentData.items[2].description",
|
"field": "documentData.items[2].description",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Operator training and documentation for Dealer Stock Crane Package",
|
"value": "Operator training and documentation for Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
"field": "documentData.items[2].unitPrice",
|
"field": "documentData.items[2].unitPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 458544,
|
"value": 312200,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -333,14 +333,14 @@
|
|||||||
"field": "documentData.items[2].totalPrice",
|
"field": "documentData.items[2].totalPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 458544,
|
"value": 312200,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.items[3].id",
|
"field": "documentData.items[3].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "63d82897-1660-4f49-b0d1-fddb9712922f",
|
"value": "4b5a4cef-3005-4253-9638-8c3cc9fade15",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
"field": "documentData.items[3].description",
|
"field": "documentData.items[3].description",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Project management and testing for Dealer Stock Crane Package",
|
"value": "Project management and testing for Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
"field": "documentData.items[3].unitPrice",
|
"field": "documentData.items[3].unitPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 320981,
|
"value": 218540,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
"field": "documentData.items[3].totalPrice",
|
"field": "documentData.items[3].totalPrice",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": 320981,
|
"value": 218540,
|
||||||
"message": "Scalar value is populated"
|
"message": "Scalar value is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -403,21 +403,21 @@
|
|||||||
"field": "documentData.quotationCustomers[0].id",
|
"field": "documentData.quotationCustomers[0].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "7a20818a-48a1-4eed-a779-ac5ec9d6159b",
|
"value": "04ff8d80-b200-4c01-8081-24d548a2a576",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotationCustomers[0].customerId",
|
"field": "documentData.quotationCustomers[0].customerId",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "56f1f9a3-6204-4ccc-af0f-5dbf866d5a56",
|
"value": "96954c53-fd37-4d97-80e0-1809756a015d",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.quotationCustomers[0].role",
|
"field": "documentData.quotationCustomers[0].role",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "09320e31-e8f9-42cd-a3ea-577210f29501",
|
"value": "a5d93b6c-131b-4e9e-9240-ac94fe12f3d8",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -438,7 +438,7 @@
|
|||||||
"field": "documentData.topics.all[0].id",
|
"field": "documentData.topics.all[0].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "ef076be2-fbef-444a-b7cf-cc2523e8b7dc",
|
"value": "58c2d002-1c6f-40c4-b68a-c11ece1fff33",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -473,7 +473,7 @@
|
|||||||
"field": "documentData.topics.all[0].items[0].content",
|
"field": "documentData.topics.all[0].items[0].content",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Supply and execute dealer stock crane package according to approved drawing.",
|
"value": "Supply and execute material handling lab crane according to approved drawing.",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -501,7 +501,7 @@
|
|||||||
"field": "documentData.topics.all[1].id",
|
"field": "documentData.topics.all[1].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "14f41b98-f9d8-463b-9880-8e96d258a6ba",
|
"value": "5a069f2d-dfb1-4ec0-9f51-3f9b657e7731",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -564,7 +564,7 @@
|
|||||||
"field": "documentData.topics.all[2].id",
|
"field": "documentData.topics.all[2].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "54500c11-5f51-49d6-947e-db2f4d689959",
|
"value": "d16b5a95-d811-4860-9670-596bef0c13df",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -634,7 +634,7 @@
|
|||||||
"field": "documentData.topics.exclusion[0].id",
|
"field": "documentData.topics.exclusion[0].id",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "14f41b98-f9d8-463b-9880-8e96d258a6ba",
|
"value": "5a069f2d-dfb1-4ec0-9f51-3f9b657e7731",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -732,14 +732,14 @@
|
|||||||
"field": "documentData.pdfme.quotation_date",
|
"field": "documentData.pdfme.quotation_date",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Jun 3, 2026",
|
"value": "Jun 15, 2026",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.pdfme.quotation_price",
|
"field": "documentData.pdfme.quotation_price",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "THB 4,418,000.00",
|
"value": "THB 3,008,000.00",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -802,49 +802,49 @@
|
|||||||
"field": "documentData.signatures.preparedBy.name",
|
"field": "documentData.signatures.preparedBy.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "Sales Crane UAT",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.preparedBy.position",
|
"field": "documentData.signatures.preparedBy.position",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Sales Manager",
|
"value": "Sales Engineer",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.preparedBy.actedAt",
|
"field": "documentData.signatures.preparedBy.actedAt",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "2026-06-03T09:00:00.000Z",
|
"value": "2026-06-15T09:00:00.000Z",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.approvedBy.name",
|
"field": "documentData.signatures.approvedBy.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "-",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.approvedBy.position",
|
"field": "documentData.signatures.approvedBy.position",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Sales Manager",
|
"value": "-",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.approvedBy.actedAt",
|
"field": "documentData.signatures.approvedBy.actedAt",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "WARNING",
|
||||||
"value": "2026-06-06T10:00:00.000Z",
|
"value": null,
|
||||||
"message": "String is populated"
|
"message": "Value is null"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "documentData.signatures.authorizedBy.name",
|
"field": "documentData.signatures.authorizedBy.name",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "Sales Manager UAT",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -858,28 +858,28 @@
|
|||||||
"field": "documentData.signatures.authorizedBy.actedAt",
|
"field": "documentData.signatures.authorizedBy.actedAt",
|
||||||
"source": "documentData",
|
"source": "documentData",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "2026-06-07T09:00:00.000Z",
|
"value": "2026-06-17T11:00:00.000Z",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.customer_name",
|
"field": "templateInput.customer_name",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "บริษัท นอร์ธเทิร์น แมชชีนเนอรี่ ดีลเลอร์ จำกัด",
|
"value": "มหาวิทยาลัยเทคโนโลยีพระจอมเกล้าพระนครเหนือ",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.customer_addr",
|
"field": "templateInput.customer_addr",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Mueang Chiang Mai, Chiang Mai",
|
"value": "Bang Sue, Bangkok",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.customer_tel",
|
"field": "templateInput.customer_tel",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "02-400824",
|
"value": "02-400827",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -893,35 +893,35 @@
|
|||||||
"field": "templateInput.customer_att",
|
"field": "templateInput.customer_att",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "คุณกิตติพงศ์ สุวรรณ",
|
"value": "คุณวราภรณ์ เกษมศรี",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.project_name",
|
"field": "templateInput.project_name",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Dealer Stock Crane Package",
|
"value": "Material Handling Lab Crane",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.site_location",
|
"field": "templateInput.site_location",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Chiang Mai Service Warehouse",
|
"value": "Bang Sue Engineering Faculty",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.quotation_date_data",
|
"field": "templateInput.quotation_date_data",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Jun 3, 2026",
|
"value": "Jun 15, 2026",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.quotation_code_data",
|
"field": "templateInput.quotation_code_data",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "QT2606-1122",
|
"value": "CR2606-1124",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -935,7 +935,7 @@
|
|||||||
"field": "templateInput.quotation_price",
|
"field": "templateInput.quotation_price",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "THB 4,418,000.00",
|
"value": "THB 3,008,000.00",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -984,35 +984,35 @@
|
|||||||
"field": "templateInput.app1",
|
"field": "templateInput.app1",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "Sales Crane UAT",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.app1_position",
|
"field": "templateInput.app1_position",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Sales Manager",
|
"value": "Sales Engineer",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.app2",
|
"field": "templateInput.app2",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "-",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.app2_position",
|
"field": "templateInput.app2_position",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Sales Manager",
|
"value": "-",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "templateInput.app3",
|
"field": "templateInput.app3",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Super Admin",
|
"value": "Sales Manager UAT",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1026,7 +1026,7 @@
|
|||||||
"field": "templateInput.quotation_date",
|
"field": "templateInput.quotation_date",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "Jun 3, 2026",
|
"value": "Jun 15, 2026",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1075,7 +1075,7 @@
|
|||||||
"field": "templateInput.quotation_code",
|
"field": "templateInput.quotation_code",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "QT2606-1122",
|
"value": "CR2606-1124",
|
||||||
"message": "String is populated"
|
"message": "String is populated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1138,7 +1138,7 @@
|
|||||||
"field": "templateInput.quotation_price_data[0][1]",
|
"field": "templateInput.quotation_price_data[0][1]",
|
||||||
"source": "templateInput",
|
"source": "templateInput",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"value": "฿4,418,000.00",
|
"value": "฿3,008,000.00",
|
||||||
"message": "Price table amount cell is formatted"
|
"message": "Price table amount cell is formatted"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"audit": "approved-snapshot-parity",
|
"audit": "approved-snapshot-parity",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"approvedSnapshotAvailable": true,
|
"approvedSnapshotAvailable": true,
|
||||||
"approvedTemplateVersionId": "86f99414-4656-44a9-9937-8457794dc710",
|
"approvedTemplateVersionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||||
"computedTemplateVersionId": "86f99414-4656-44a9-9937-8457794dc710",
|
"computedTemplateVersionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||||
"inconsistentTemplateKeys": [],
|
"inconsistentTemplateKeys": [],
|
||||||
"computedSignatureHash": "72c05979b9a0b0666a06cce5b80dc67fe9c5493eced4366a4cf63785dc415269",
|
"computedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||||
"storedSignatureHash": "72c05979b9a0b0666a06cce5b80dc67fe9c5493eced4366a4cf63785dc415269",
|
"storedSignatureHash": "922ab2a823f98b032b17da281c86c3a2780d27cca73a60a7b983d14ad039b229",
|
||||||
"approvedArtifactReference": "artifact:ec300b87-3710-43a4-a63c-a72168fe3933",
|
"approvedArtifactReference": null,
|
||||||
"refreshedFixtureSnapshot": true,
|
"refreshedFixtureSnapshot": true,
|
||||||
"usedAuditFixture": false
|
"usedAuditFixture": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-06-30T01:35:49.157Z",
|
"generatedAt": "2026-06-30T02:56:03.783Z",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
"templateVersionReport": "template-version-report.json",
|
"templateVersionReport": "template-version-report.json",
|
||||||
"placeholderIntegrityReport": "placeholder-integrity-report.json",
|
"placeholderIntegrityReport": "placeholder-integrity-report.json",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# PDF Integrity Summary
|
# PDF Integrity Summary
|
||||||
|
|
||||||
- Generated At: 2026-06-30T01:35:49.157Z
|
- Generated At: 2026-06-30T02:56:03.783Z
|
||||||
- Overall Status: PASS
|
- Overall Status: PASS
|
||||||
- Quotation Code: QT2606-1122
|
- Quotation Code: CR2606-1124
|
||||||
|
|
||||||
## Statuses
|
## Statuses
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"audit": "template-version-integrity",
|
"audit": "template-version-integrity",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"activeTemplateCount": 2,
|
"activeTemplateCount": 1,
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"approvedTemplateVersionId": null,
|
"approvedTemplateVersionId": null,
|
||||||
"approvedTemplateVersionIsActive": false,
|
"approvedTemplateVersionIsActive": false,
|
||||||
"issues": []
|
"issues": []
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"audit": "topic-runtime",
|
"audit": "topic-runtime",
|
||||||
"overallStatus": "PASS",
|
"overallStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"topicCount": 3,
|
"topicCount": 3,
|
||||||
"topicInputKeys": [
|
"topicInputKeys": [
|
||||||
"topic_1_0",
|
"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",
|
"overallStatus": "PASS",
|
||||||
"quotationCode": "QT2606-1122",
|
"quotationCode": "CR2606-1124",
|
||||||
"expectedFixtureCode": "QT-H5-AUDIT",
|
"expectedFixtureCode": "QT-H5-AUDIT",
|
||||||
"template": {
|
"template": {
|
||||||
"templateName": "ALLA Quotation Standard",
|
"templateName": "ALLA Demo Organization Quotation Standard",
|
||||||
"version": "2.1",
|
"version": "1.0",
|
||||||
"versionId": "86f99414-4656-44a9-9937-8457794dc710",
|
"versionId": "850b13a2-c9ae-4992-89e7-a43f50c34378",
|
||||||
"fieldCount": 44,
|
"fieldCount": 44,
|
||||||
"previousVersion": "2.0",
|
"previousVersion": null,
|
||||||
"deletedFields": [
|
"deletedFields": [],
|
||||||
"__section_role__customer",
|
"newFields": [
|
||||||
"__section_role__product_items",
|
"Please_do_not",
|
||||||
"__section_role__signature",
|
"app1",
|
||||||
"__section_role__topics",
|
"app1_position",
|
||||||
"items_table",
|
"app2",
|
||||||
"product_items_currency_value",
|
"app2_position",
|
||||||
"product_items_footer_note",
|
"app3",
|
||||||
"product_items_line_168",
|
"app3_position",
|
||||||
"product_items_line_35",
|
"att_label",
|
||||||
"product_items_subtitle",
|
"colon",
|
||||||
"product_items_title",
|
"colon copy",
|
||||||
"product_items_total_label",
|
"colon_att",
|
||||||
"product_items_total_value"
|
"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": [],
|
"duplicateFields": [],
|
||||||
"unknownFields": [],
|
"unknownFields": [],
|
||||||
"classifications": [
|
"classifications": [
|
||||||
@@ -232,7 +263,7 @@
|
|||||||
"consistency": {
|
"consistency": {
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"approvedSnapshotAvailable": true,
|
"approvedSnapshotAvailable": true,
|
||||||
"approvedArtifactReference": "artifact:ec300b87-3710-43a4-a63c-a72168fe3933",
|
"approvedArtifactReference": null,
|
||||||
"inconsistentKeys": []
|
"inconsistentKeys": []
|
||||||
},
|
},
|
||||||
"integritySummary": {
|
"integritySummary": {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# PDF Audit Report
|
# PDF Audit Report
|
||||||
|
|
||||||
- Generated At: 2026-06-30T01:35:48.376Z
|
- Generated At: 2026-06-30T02:56:00.835Z
|
||||||
- Overall Status: PASS
|
- Overall Status: PASS
|
||||||
- Quotation Code: QT2606-1122
|
- Quotation Code: CR2606-1124
|
||||||
- Template: ALLA Quotation Standard v2.1
|
- Template: ALLA Demo Organization Quotation Standard v1.0
|
||||||
- Previous Version: 2.0
|
- Previous Version: None
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,21 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
ALLOW_DB_RESET=true npm run db:fresh
|
npm run db:fresh
|
||||||
npm exec tsc --noEmit
|
npm run typecheck
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fresh UAT or demo data
|
## Fresh UAT or demo data
|
||||||
|
|
||||||
```bash
|
```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
|
## Existing database upgrade
|
||||||
@@ -44,7 +50,7 @@ npm run seed:pdf-template
|
|||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm exec tsc --noEmit
|
npm run typecheck
|
||||||
npm run build
|
npm run build
|
||||||
npm run verify:encoding
|
npm run verify:encoding
|
||||||
npm run verify:crm-access
|
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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
37
package.json
37
package.json
@@ -12,47 +12,42 @@
|
|||||||
"format": "oxfmt --write .",
|
"format": "oxfmt --write .",
|
||||||
"format:check": "oxfmt --check .",
|
"format:check": "oxfmt --check .",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"gen": "npm run verify:encoding && npx drizzle-kit generate",
|
"typecheck": "tsc --noEmit",
|
||||||
"db:generate": "npx drizzle-kit generate",
|
"db:generate": "npx drizzle-kit generate",
|
||||||
"db:migrate": "npx drizzle-kit migrate",
|
"db:migrate": "npx drizzle-kit migrate",
|
||||||
"db:studio": "npx drizzle-kit studio",
|
"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": "npm run db:migrate && npm run seed:system",
|
||||||
"db:setup:uat": "npm run db:setup && npm run seed:uat",
|
"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": "npm run db:reset -- --allow-db-reset && npm run db:migrate && npm run seed:system",
|
||||||
"db:fresh:uat": "npm run db:fresh && npm run seed:uat",
|
"db:fresh:uat": "npm run db:reset -- --allow-db-reset && npm run db:migrate && npm run seed:system && npm run seed:uat",
|
||||||
"migrate": "npm run db:migrate",
|
"setup:db": "npm run db:setup",
|
||||||
"studio": "npm run db:studio",
|
|
||||||
"seed:super-admin": "node scripts/seed-super-admin.js",
|
"seed:super-admin": "node scripts/seed-super-admin.js",
|
||||||
"seed:foundation": "node --experimental-strip-types src/db/seeds/foundation.seed.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 --experimental-strip-types src/db/seeds/crm-uat.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:pdf-template-version": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts",
|
"seed:system": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-system.ts",
|
||||||
"seed:system": "node --experimental-strip-types src/db/seeds/system.seed.ts",
|
"seed:uat": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-uat.ts",
|
||||||
"seed:uat": "node --experimental-strip-types src/db/seeds/uat.seed.ts",
|
"seed:reset": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/seed-reset.ts",
|
||||||
"seed:pdf-template": "node --experimental-strip-types scripts/reseed-pdf-template-version.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",
|
"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",
|
"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-h1": "node scripts/verify-task-h1.js",
|
||||||
"verify:task-h3": "node --experimental-strip-types scripts/verify-task-h3.js",
|
"verify:task-h3": "node 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:crm-access": "node --experimental-strip-types scripts/security/verify-crm-access.ts",
|
"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: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: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:runtime": "node --experimental-strip-types scripts/audit-pdf-runtime-payload.ts",
|
||||||
"audit:pdf:report": "node --experimental-strip-types scripts/generate-pdf-audit-report.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: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:placeholders": "node --experimental-strip-types scripts/audit-placeholder-integrity.ts",
|
||||||
"audit:pdf:parity": "node --experimental-strip-types scripts/audit-payload-parity.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: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: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": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts",
|
||||||
"audit:pdf:visual:baseline": "npx tsx scripts/audit-pdf-visual-regression.ts -- --write-baseline",
|
"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",
|
"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"
|
"pdf:activate:product": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts --template-variant=product-v1 --activate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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'));
|
loadEnvFile(path.resolve(process.cwd(), '.env'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ALLOW_DB_RESET_FLAG = '--allow-db-reset';
|
||||||
|
|
||||||
function assertSafeToReset(databaseUrl: string) {
|
function assertSafeToReset(databaseUrl: string) {
|
||||||
if (process.env.ALLOW_DB_RESET !== 'true') {
|
const allowDbReset =
|
||||||
throw new Error('ALLOW_DB_RESET=true is required.');
|
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') {
|
if ((process.env.NODE_ENV ?? '').toLowerCase() === 'production') {
|
||||||
throw new Error('Database reset is blocked in NODE_ENV=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';
|
"use client";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -7,8 +11,8 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -22,18 +26,18 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
SidebarRail
|
SidebarRail,
|
||||||
} from '@/components/ui/sidebar';
|
} from "@/components/ui/sidebar";
|
||||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||||
import { navGroups } from '@/config/nav-config';
|
import { navGroups } from "@/config/nav-config";
|
||||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { useFilteredNavGroups } from '@/hooks/use-nav';
|
import { useFilteredNavGroups } from "@/hooks/use-nav";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Icons } from '../icons';
|
import { Icons } from "../icons";
|
||||||
import { OrgSwitcher } from '../org-switcher';
|
import { OrgSwitcher } from "../org-switcher";
|
||||||
|
|
||||||
export default function AppSidebar() {
|
export default function AppSidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -44,59 +48,67 @@ export default function AppSidebar() {
|
|||||||
const user = session?.user ?? null;
|
const user = session?.user ?? null;
|
||||||
const organization = session?.user?.activeOrganizationId;
|
const organization = session?.user?.activeOrganizationId;
|
||||||
const canManageOrganization =
|
const canManageOrganization =
|
||||||
session?.user?.systemRole === 'super_admin' ||
|
session?.user?.systemRole === "super_admin" ||
|
||||||
(!!organization && session?.user?.activeMembershipRole === 'admin');
|
(!!organization && session?.user?.activeMembershipRole === "admin");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Side effects based on sidebar state changes
|
// Side effects based on sidebar state changes
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
if (process.env.NODE_ENV !== "development") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[sidebar-debug]', {
|
console.info("[sidebar-debug]", {
|
||||||
systemRole: session?.user?.systemRole ?? null,
|
systemRole: session?.user?.systemRole ?? null,
|
||||||
activeOrganizationId: session?.user?.activeOrganizationId ?? null,
|
activeOrganizationId: session?.user?.activeOrganizationId ?? null,
|
||||||
activeMembershipRole: session?.user?.activeMembershipRole ?? null,
|
activeMembershipRole: session?.user?.activeMembershipRole ?? null,
|
||||||
activePermissions: session?.user?.activePermissions ?? [],
|
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,
|
filteredGroups,
|
||||||
session?.user?.activeOrganizationId,
|
session?.user?.activeOrganizationId,
|
||||||
session?.user?.activePermissions,
|
session?.user?.activePermissions,
|
||||||
session?.user?.activeMembershipRole,
|
session?.user?.activeMembershipRole,
|
||||||
session?.user?.systemRole
|
session?.user?.systemRole,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible='icon'>
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader className='group-data-[collapsible=icon]:pt-4'>
|
<SidebarHeader className="group-data-[collapsible=icon]:pt-4">
|
||||||
<OrgSwitcher />
|
<OrgSwitcher />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className='overflow-x-hidden'>
|
<SidebarContent className="overflow-x-hidden">
|
||||||
{filteredGroups.map((group) => (
|
{filteredGroups.map((group) => (
|
||||||
<SidebarGroup key={group.label || 'ungrouped'} className='py-0'>
|
<SidebarGroup key={group.label || "ungrouped"} className="py-0">
|
||||||
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
{group.label && (
|
||||||
|
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
||||||
|
)}
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{group.items.map((item) => {
|
{group.items.map((item) => {
|
||||||
const Icon = item.icon ? Icons[item.icon] : Icons.logo;
|
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 ? (
|
return item?.items && item?.items?.length > 0 ? (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
key={itemKey}
|
key={itemKey}
|
||||||
asChild
|
asChild
|
||||||
defaultOpen={item.isActive}
|
defaultOpen={item.isActive}
|
||||||
className='group/collapsible'
|
className="group/collapsible"
|
||||||
>
|
>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton tooltip={item.title} isActive={pathname === item.url}>
|
<SidebarMenuButton
|
||||||
|
tooltip={item.title}
|
||||||
|
isActive={pathname === item.url}
|
||||||
|
>
|
||||||
{item.icon && <Icon />}
|
{item.icon && <Icon />}
|
||||||
<span>{item.title}</span>
|
<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>
|
</SidebarMenuButton>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
@@ -105,7 +117,10 @@ export default function AppSidebar() {
|
|||||||
<SidebarMenuSubItem
|
<SidebarMenuSubItem
|
||||||
key={subItem.url || `${itemKey}-${subItem.title}`}
|
key={subItem.url || `${itemKey}-${subItem.title}`}
|
||||||
>
|
>
|
||||||
<SidebarMenuSubButton asChild isActive={pathname === subItem.url}>
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
isActive={pathname === subItem.url}
|
||||||
|
>
|
||||||
<Link href={subItem.url}>
|
<Link href={subItem.url}>
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -141,49 +156,57 @@ export default function AppSidebar() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size='lg'
|
size="lg"
|
||||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
{user && (
|
{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>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
side='bottom'
|
side="bottom"
|
||||||
align='end'
|
align="end"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className='p-0 font-normal'>
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
<div className='px-1 py-1.5'>
|
<div className="px-1 py-1.5">
|
||||||
{user && (
|
{user && (
|
||||||
<UserAvatarProfile className='h-8 w-8 rounded-lg' showInfo user={user} />
|
<UserAvatarProfile
|
||||||
|
className="h-8 w-8 rounded-lg"
|
||||||
|
showInfo
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
<DropdownMenuItem
|
||||||
<Icons.account className='mr-2 h-4 w-4' />
|
onClick={() => router.push("/dashboard/profile")}
|
||||||
|
>
|
||||||
|
<Icons.account className="mr-2 h-4 w-4" />
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{canManageOrganization && (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={() => router.push('/dashboard/billing')}>
|
onClick={() => router.push("/dashboard/notifications")}
|
||||||
<Icons.creditCard className='mr-2 h-4 w-4' />
|
>
|
||||||
Billing
|
<Icons.notification className="mr-2 h-4 w-4" />
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem onClick={() => router.push('/dashboard/notifications')}>
|
|
||||||
<Icons.notification className='mr-2 h-4 w-4' />
|
|
||||||
Notifications
|
Notifications
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
<DropdownMenuItem
|
||||||
<Icons.logout className='mr-2 h-4 w-4' />
|
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||||
|
>
|
||||||
|
<Icons.logout className="mr-2 h-4 w-4" />
|
||||||
Sign out
|
Sign out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -7,11 +7,11 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||||
import { signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
export function UserNav() {
|
export function UserNav() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
@@ -21,28 +21,37 @@ export function UserNav() {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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} />
|
<UserAvatarProfile user={user} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className='w-56' align='end' sideOffset={10} forceMount>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel className='font-normal'>
|
className="w-56"
|
||||||
<div className='flex flex-col space-y-1'>
|
align="end"
|
||||||
<p className='text-sm leading-none font-medium'>{user.name}</p>
|
sideOffset={10}
|
||||||
<p className='text-muted-foreground text-xs leading-none'>{user.email}</p>
|
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>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
<DropdownMenuItem onClick={() => router.push("/dashboard/profile")}>
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>Billing</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||||
|
>
|
||||||
Sign out
|
Sign out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ type UserContext = {
|
|||||||
admin: UserMembershipRow;
|
admin: UserMembershipRow;
|
||||||
marketing: UserMembershipRow;
|
marketing: UserMembershipRow;
|
||||||
sales: UserMembershipRow[];
|
sales: UserMembershipRow[];
|
||||||
|
salesByProductType: Partial<Record<string, UserMembershipRow>>;
|
||||||
salesManager: UserMembershipRow;
|
salesManager: UserMembershipRow;
|
||||||
departmentManager: UserMembershipRow;
|
departmentManager: UserMembershipRow;
|
||||||
topManager: UserMembershipRow;
|
topManager: UserMembershipRow;
|
||||||
@@ -352,33 +353,41 @@ type UserContext = {
|
|||||||
|
|
||||||
const SEED_TAG = "crm-uat-seed-v1";
|
const SEED_TAG = "crm-uat-seed-v1";
|
||||||
const PERIOD = "2606";
|
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[] = [
|
const STORIES: StoryConfig[] = [
|
||||||
{
|
{
|
||||||
slug: "website-warehouse-crane-win",
|
slug: "website-warehouse-crane-win",
|
||||||
title: "Lead received from website and won after approval",
|
title: "Siam Manufacturing crane project waiting final CEO approval",
|
||||||
customerName: "บริษัท บางกอก ฟู้ด อินดัสทรี จำกัด",
|
customerName: "บริษัท บางกอก ฟู้ด อินดัสทรี จำกัด",
|
||||||
province: "Bangkok",
|
province: "Chachoengsao",
|
||||||
district: "Min Buri",
|
district: "Bang Pakong",
|
||||||
branchCode: "head_office",
|
branchCode: "head_office",
|
||||||
customerGroupCode: "industrial",
|
customerGroupCode: "industrial",
|
||||||
customerSubGroupCode: "factory",
|
customerSubGroupCode: "factory",
|
||||||
productTypeCode: "crane",
|
productTypeCode: "crane",
|
||||||
quotationTypeCode: "crane",
|
quotationTypeCode: "crane",
|
||||||
priorityCode: "high",
|
priorityCode: "high",
|
||||||
leadChannelCode: "website",
|
leadChannelCode: "referral",
|
||||||
awarenessCode: "google_search_website",
|
awarenessCode: "google_search_website",
|
||||||
leadStatusCode: "assigned",
|
leadStatusCode: "assigned",
|
||||||
leadFollowupStatusCode: "waiting_po",
|
leadFollowupStatusCode: "drawing_followup",
|
||||||
opportunityStatusCode: "negotiation",
|
opportunityStatusCode: "negotiation",
|
||||||
chancePercent: 80,
|
chancePercent: 78,
|
||||||
estimatedValue: 12400000,
|
estimatedValue: 8750000,
|
||||||
projectName: "Warehouse Crane Installation Phase 2",
|
projectName: "Overhead Crane Installation Phase 1",
|
||||||
projectLocation: "Bangkok Food Industrial Estate, Min Buri",
|
projectLocation: "Toyota Gateway Plant",
|
||||||
leadDescription:
|
leadDescription:
|
||||||
"Customer asked for top-running overhead crane to support raw material warehouse expansion.",
|
"Marketing qualified the crane project and handed it to the crane sales queue for quotation approval.",
|
||||||
quotePlan: "accepted",
|
quotePlan: "pending_step_2",
|
||||||
opportunityOutcome: "won",
|
opportunityOutcome: "open",
|
||||||
multiParties: [
|
multiParties: [
|
||||||
{
|
{
|
||||||
roleCode: "contractor",
|
roleCode: "contractor",
|
||||||
@@ -392,57 +401,57 @@ const STORIES: StoryConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "facebook-lost-price",
|
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: "บริษัท ไทย ออโต้ เพรส จำกัด",
|
customerName: "บริษัท ไทย ออโต้ เพรส จำกัด",
|
||||||
province: "Samut Prakan",
|
province: "Bangkok",
|
||||||
district: "Bang Phli",
|
district: "Lat Krabang",
|
||||||
branchCode: "factory",
|
branchCode: "service",
|
||||||
customerGroupCode: "industrial",
|
customerGroupCode: "industrial",
|
||||||
customerSubGroupCode: "factory",
|
customerSubGroupCode: "warehouse",
|
||||||
productTypeCode: "crane",
|
productTypeCode: "dockdoor",
|
||||||
quotationTypeCode: "crane",
|
quotationTypeCode: "dockdoor",
|
||||||
priorityCode: "normal",
|
priorityCode: "normal",
|
||||||
leadChannelCode: "facebook",
|
leadChannelCode: "website",
|
||||||
awarenessCode: "social_media",
|
awarenessCode: "google_search_website",
|
||||||
leadStatusCode: "closed_lost",
|
leadStatusCode: "cancel",
|
||||||
leadFollowupStatusCode: "quotation_preparation",
|
leadFollowupStatusCode: "budget_pending",
|
||||||
opportunityStatusCode: "quotation_created",
|
opportunityStatusCode: "qualification",
|
||||||
chancePercent: 45,
|
chancePercent: 20,
|
||||||
estimatedValue: 5800000,
|
estimatedValue: 1900000,
|
||||||
projectName: "Stamping Line Hoist Replacement",
|
projectName: "Budget Hold Demo Project",
|
||||||
projectLocation: "Bang Phli Industrial Estate",
|
projectLocation: "Lat Krabang Logistics Zone",
|
||||||
leadDescription:
|
leadDescription:
|
||||||
"Customer compared 5 ton monorail hoist replacement between ALLA and Konecranes.",
|
"Opportunity was stopped before quotation because customer budget was not approved and competitor was still under consideration.",
|
||||||
competitor: "Konecranes",
|
competitor: "Competitor Selected",
|
||||||
quotePlan: "sent_follow_up",
|
quotePlan: "cancelled",
|
||||||
opportunityOutcome: "lost",
|
opportunityOutcome: "no_quotation",
|
||||||
lostReasonCode: "price_too_high",
|
noQuotationReasonCode: "budget_not_ready",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "revision-r02-approved",
|
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: "บริษัท กรีน โลจิสติกส์ พาร์ค จำกัด",
|
customerName: "บริษัท กรีน โลจิสติกส์ พาร์ค จำกัด",
|
||||||
province: "Pathum Thani",
|
province: "Chonburi",
|
||||||
district: "Khlong Luang",
|
district: "Si Racha",
|
||||||
branchCode: "service",
|
branchCode: "service",
|
||||||
customerGroupCode: "industrial",
|
customerGroupCode: "industrial",
|
||||||
customerSubGroupCode: "logistics",
|
customerSubGroupCode: "warehouse",
|
||||||
productTypeCode: "dockdoor",
|
productTypeCode: "dockdoor",
|
||||||
quotationTypeCode: "dockdoor",
|
quotationTypeCode: "dockdoor",
|
||||||
priorityCode: "high",
|
priorityCode: "high",
|
||||||
leadChannelCode: "referral",
|
leadChannelCode: "referral",
|
||||||
awarenessCode: "word_of_mouth",
|
awarenessCode: "word_of_mouth",
|
||||||
leadStatusCode: "assigned",
|
leadStatusCode: "assigned",
|
||||||
leadFollowupStatusCode: "waiting_po",
|
leadFollowupStatusCode: "quotation_preparation",
|
||||||
opportunityStatusCode: "negotiation",
|
opportunityStatusCode: "proposal_preparation",
|
||||||
chancePercent: 70,
|
chancePercent: 61,
|
||||||
estimatedValue: 9300000,
|
estimatedValue: 4600000,
|
||||||
projectName: "Cold Chain Dock Leveller Upgrade",
|
projectName: "Loading Dock Expansion",
|
||||||
projectLocation: "Khlong Luang Logistics Campus",
|
projectLocation: "WHA Warehouse",
|
||||||
leadDescription:
|
leadDescription:
|
||||||
"Customer requested revision after consultant changed dock layout and bumper specification.",
|
"Dock door salesperson is still following up layout confirmation before final quotation submission.",
|
||||||
quotePlan: "rejected_then_revised",
|
quotePlan: "sent_follow_up",
|
||||||
opportunityOutcome: "won",
|
opportunityOutcome: "open",
|
||||||
multiParties: [
|
multiParties: [
|
||||||
{
|
{
|
||||||
roleCode: "consultant",
|
roleCode: "consultant",
|
||||||
@@ -882,6 +891,30 @@ function buildDocumentCode(prefix: string, number: number) {
|
|||||||
return `${prefix}${PERIOD}-${String(number).padStart(3, "0")}`;
|
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) {
|
function seededNote(scope: string) {
|
||||||
return `[${SEED_TAG}:${scope}]`;
|
return `[${SEED_TAG}:${scope}]`;
|
||||||
}
|
}
|
||||||
@@ -1027,21 +1060,48 @@ function buildUserContext(rows: UserMembershipRow[]): UserContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const first = rows[0];
|
const first = rows[0];
|
||||||
const byRole = (role: string) =>
|
const byRole = (role: string) => rows.find((row) => row.businessRole === role) ?? null;
|
||||||
rows.find((row) => row.businessRole === role) ?? null;
|
const byEmail = (email: string) =>
|
||||||
const salesRows = rows.filter((row) => row.businessRole === "sales");
|
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 =
|
const admin =
|
||||||
|
byEmail(UAT_EMAILS.admin) ??
|
||||||
rows.find((row) => row.membershipRole === "admin") ??
|
rows.find((row) => row.membershipRole === "admin") ??
|
||||||
rows.find((row) => row.systemRole === "super_admin") ??
|
rows.find((row) => row.systemRole === "super_admin") ??
|
||||||
first;
|
first;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
admin,
|
admin,
|
||||||
marketing: byRole("sales_support") ?? admin,
|
marketing:
|
||||||
sales: salesRows.length ? salesRows : [admin],
|
byEmail(UAT_EMAILS.marketing) ??
|
||||||
salesManager: byRole("sales_manager") ?? admin,
|
byRole("marketing") ??
|
||||||
departmentManager: byRole("department_manager") ?? admin,
|
byRole("sales_support") ??
|
||||||
topManager: byRole("top_manager") ?? admin,
|
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 contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
||||||
const createdAt = daysAgo(88 - storyIndex * 4);
|
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 assignedAt = addDays(createdAt, 4);
|
||||||
const statusId = getOptionId(
|
const statusId = getOptionId(
|
||||||
optionMap,
|
optionMap,
|
||||||
@@ -2004,7 +2064,7 @@ function buildQuotationData(
|
|||||||
const customer = storyCustomerMap.get(story.slug);
|
const customer = storyCustomerMap.get(story.slug);
|
||||||
if (!opportunity || !customer) continue;
|
if (!opportunity || !customer) continue;
|
||||||
const contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
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 baseDate = addDays(opportunity.createdAt, 10);
|
||||||
const planRevisionCount =
|
const planRevisionCount =
|
||||||
story.quotePlan === "returned_then_revised"
|
story.quotePlan === "returned_then_revised"
|
||||||
@@ -2067,7 +2127,7 @@ function buildQuotationData(
|
|||||||
|
|
||||||
quotations.push({
|
quotations.push({
|
||||||
id: quotationId,
|
id: quotationId,
|
||||||
code: buildDocumentCode("QT", quotationSequence++),
|
code: buildQuotationCode(story.productTypeCode, quotationSequence++),
|
||||||
opportunityId: opportunity.id,
|
opportunityId: opportunity.id,
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
contactId: contact?.id ?? null,
|
contactId: contact?.id ?? null,
|
||||||
@@ -2405,7 +2465,10 @@ function buildQuotationData(
|
|||||||
const quotationId = crypto.randomUUID();
|
const quotationId = crypto.randomUUID();
|
||||||
quotations.push({
|
quotations.push({
|
||||||
id: quotationId,
|
id: quotationId,
|
||||||
code: buildDocumentCode("QT", quotationSequence++),
|
code: buildQuotationCode(
|
||||||
|
choose(["crane", "dockdoor", "solarcell"], quotations.length),
|
||||||
|
quotationSequence++
|
||||||
|
),
|
||||||
opportunityId: null,
|
opportunityId: null,
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
contactId: contact?.id ?? null,
|
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';
|
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[]) {
|
function run(label: string, args: string[]) {
|
||||||
const result = spawnSync(process.execPath, args, {
|
const result = spawnSync(process.execPath, args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
@@ -13,7 +19,12 @@ function run(label: string, args: string[]) {
|
|||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
run('super admin seed', ['scripts/seed-super-admin.js']);
|
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();
|
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';
|
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() {
|
function main() {
|
||||||
const result = spawnSync(
|
const steps: Array<{ label: string; args: string[] }> = [
|
||||||
process.execPath,
|
|
||||||
['--experimental-strip-types', 'src/db/seeds/crm-uat.seed.ts'],
|
|
||||||
{
|
{
|
||||||
|
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',
|
stdio: 'inherit',
|
||||||
env: process.env
|
env: process.env
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
throw new Error(`[seed:uat] failed with exit code ${result.status ?? 1}`);
|
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() {
|
export default function SignInViewPage() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard/crm";
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ async function canAccessCustomerRow(
|
|||||||
accessContext,
|
accessContext,
|
||||||
));
|
));
|
||||||
|
|
||||||
if (accessContext.ownershipScope === "monitor") {
|
if (accessContext.ownershipScope === 'monitor') {
|
||||||
return opportunityCustomerIds.has(row.id);
|
return opportunityCustomerIds.has(row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
src/proxy.ts
44
src/proxy.ts
@@ -1,29 +1,39 @@
|
|||||||
import { auth } from '@/auth';
|
import { auth } from "@/auth";
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
const protectedApiPrefixes = ['/api/products', '/api/organizations', '/api/users'];
|
const protectedApiPrefixes = [
|
||||||
const superAdminOnlyRoutes = ['/dashboard/workspaces'];
|
"/api/products",
|
||||||
const organizationAdminRoutes = ['/dashboard/users', '/dashboard/workspaces/team'];
|
"/api/organizations",
|
||||||
|
"/api/users",
|
||||||
|
];
|
||||||
|
const superAdminOnlyRoutes = ["/dashboard/workspaces"];
|
||||||
|
const organizationAdminRoutes = [
|
||||||
|
"/dashboard/users",
|
||||||
|
"/dashboard/workspaces/team",
|
||||||
|
];
|
||||||
|
|
||||||
export default auth((req) => {
|
export default auth((req) => {
|
||||||
const pathname = req.nextUrl.pathname;
|
const pathname = req.nextUrl.pathname;
|
||||||
const isDashboardRoute = pathname.startsWith('/dashboard');
|
const isDashboardRoute = pathname.startsWith("/dashboard/crm");
|
||||||
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) => pathname.startsWith(prefix));
|
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) =>
|
||||||
|
pathname.startsWith(prefix),
|
||||||
|
);
|
||||||
const user = req.auth?.user;
|
const user = req.auth?.user;
|
||||||
const isSignedIn = !!user;
|
const isSignedIn = !!user;
|
||||||
const isSuperAdmin = user?.systemRole === 'super_admin';
|
const isSuperAdmin = user?.systemRole === "super_admin";
|
||||||
const isOrganizationAdmin =
|
const isOrganizationAdmin =
|
||||||
isSuperAdmin ||
|
isSuperAdmin ||
|
||||||
(user?.activeOrganizationId &&
|
(user?.activeOrganizationId &&
|
||||||
(user.activeMembershipRole === 'admin' || user.activePermissions?.includes('users:manage')));
|
(user.activeMembershipRole === "admin" ||
|
||||||
|
user.activePermissions?.includes("users:manage")));
|
||||||
|
|
||||||
if (!isSignedIn && (isDashboardRoute || isProtectedApiRoute)) {
|
if (!isSignedIn && (isDashboardRoute || isProtectedApiRoute)) {
|
||||||
if (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);
|
const signInUrl = new URL("/auth/sign-in", req.nextUrl.origin);
|
||||||
signInUrl.searchParams.set('callbackUrl', pathname);
|
signInUrl.searchParams.set("callbackUrl", pathname);
|
||||||
return NextResponse.redirect(signInUrl);
|
return NextResponse.redirect(signInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +42,7 @@ export default auth((req) => {
|
|||||||
superAdminOnlyRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
superAdminOnlyRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||||
!isSuperAdmin
|
!isSuperAdmin
|
||||||
) {
|
) {
|
||||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -40,7 +50,7 @@ export default auth((req) => {
|
|||||||
organizationAdminRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
organizationAdminRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||||
!isOrganizationAdmin
|
!isOrganizationAdmin
|
||||||
) {
|
) {
|
||||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -48,7 +58,7 @@ export default auth((req) => {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||||
'/(api|trpc)(.*)'
|
"/(api|trpc)(.*)",
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user