task-b.1
complere
This commit is contained in:
phaichayon
2026-06-15 11:19:31 +07:00
parent 89b39cad38
commit b8cd39eaa4
55 changed files with 4295 additions and 1848 deletions

View File

@@ -0,0 +1,298 @@
const fs = require('node:fs');
const path = require('node:path');
const postgres = require('postgres');
type SqlClient = ReturnType<typeof postgres>;
type SeedOption = {
code: string;
label: string;
value: string;
sortOrder: number;
};
type BranchSeedRow = {
id: string;
category: string;
code: string;
label: string;
};
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;
}
}
}
function loadLocalEnv() {
loadEnvFile(path.resolve(process.cwd(), '.env.local'));
loadEnvFile(path.resolve(process.cwd(), '.env'));
}
function requireEnv(name: string) {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function getCurrentPeriod(date = new Date()) {
const year = String(date.getFullYear()).slice(-2);
const month = String(date.getMonth() + 1).padStart(2, '0');
return `${year}${month}`;
}
const FOUNDATION_OPTIONS = {
crm_branch: [
{ code: 'head_office', label: 'สำนักงานใหญ่', value: 'head_office', sortOrder: 1 },
{ code: 'factory', label: 'Factory', value: 'factory', sortOrder: 2 },
{ code: 'service', label: 'Service', value: 'service', sortOrder: 3 }
],
crm_customer_status: [
{ code: 'active', label: 'Active', value: 'active', sortOrder: 1 },
{ code: 'inactive', label: 'Inactive', value: 'inactive', sortOrder: 2 },
{ code: 'suspended', label: 'Suspended', value: 'suspended', sortOrder: 3 }
],
crm_customer_type: [
{ code: 'company', label: 'Company', value: 'company', sortOrder: 1 },
{ code: 'individual', label: 'Individual', value: 'individual', sortOrder: 2 }
],
crm_enquiry_status: [
{ code: 'new', label: 'New', value: 'new', sortOrder: 1 },
{ code: 'qualifying', label: 'Qualifying', value: 'qualifying', sortOrder: 2 },
{ code: 'requirement', label: 'Requirement', value: 'requirement', sortOrder: 3 },
{ code: 'follow_up', label: 'Follow Up', value: 'follow_up', sortOrder: 4 },
{ code: 'converted', label: 'Converted', value: 'converted', sortOrder: 5 },
{ code: 'closed_lost', label: 'Closed Lost', value: 'closed_lost', sortOrder: 6 },
{ code: 'cancelled', label: 'Cancelled', value: 'cancelled', sortOrder: 7 }
],
crm_quotation_status: [
{ code: 'draft', label: 'Draft', value: 'draft', sortOrder: 1 },
{
code: 'pending_approval',
label: 'Pending Approval',
value: 'pending_approval',
sortOrder: 2
},
{ code: 'approved', label: 'Approved', value: 'approved', sortOrder: 3 },
{ code: 'rejected', label: 'Rejected', value: 'rejected', sortOrder: 4 },
{ code: 'sent', label: 'Sent', value: 'sent', sortOrder: 5 },
{ code: 'accepted', label: 'Accepted', value: 'accepted', sortOrder: 6 },
{ code: 'lost', label: 'Lost', value: 'lost', sortOrder: 7 },
{ code: 'cancelled', label: 'Cancelled', value: 'cancelled', sortOrder: 8 },
{ code: 'revised', label: 'Revised', value: 'revised', sortOrder: 9 }
],
crm_product_type: [
{ code: 'crane', label: 'Crane', value: 'crane', sortOrder: 1 },
{ code: 'dockdoor', label: 'Dock Door', value: 'dockdoor', sortOrder: 2 },
{ code: 'solarcell', label: 'Solar Cell', value: 'solarcell', sortOrder: 3 }
],
crm_currency: [
{ code: 'THB', label: 'THB', value: 'THB', sortOrder: 1 },
{ code: 'USD', label: 'USD', value: 'USD', sortOrder: 2 },
{ code: 'EUR', label: 'EUR', value: 'EUR', sortOrder: 3 }
],
crm_payment_term: [
{ code: 'cash', label: 'Cash', value: 'cash', sortOrder: 1 },
{ code: 'credit_30', label: 'Credit 30', value: 'credit_30', sortOrder: 2 },
{ code: 'credit_60', label: 'Credit 60', value: 'credit_60', sortOrder: 3 }
],
crm_priority: [
{ code: 'low', label: 'Low', value: 'low', sortOrder: 1 },
{ code: 'normal', label: 'Normal', value: 'normal', sortOrder: 2 },
{ code: 'high', label: 'High', value: 'high', sortOrder: 3 },
{ code: 'urgent', label: 'Urgent', value: 'urgent', sortOrder: 4 }
],
crm_lead_channel: [
{ code: 'website', label: 'Website', value: 'website', sortOrder: 1 },
{ code: 'referral', label: 'Referral', value: 'referral', sortOrder: 2 },
{ code: 'sales', label: 'Sales', value: 'sales', sortOrder: 3 },
{ code: 'facebook', label: 'Facebook', value: 'facebook', sortOrder: 4 },
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 },
{ code: 'other', label: 'Other', value: 'other', sortOrder: 6 }
]
};
const DOCUMENT_SEQUENCES = [
{ documentType: 'customer', prefix: 'CUS' },
{ documentType: 'contact', prefix: 'CON' },
{ documentType: 'enquiry', prefix: 'ENQ' },
{ documentType: 'quotation', prefix: 'QT' },
{ documentType: 'approval', prefix: 'APV' }
];
async function upsertMasterOptionsForOrganization(
sql: SqlClient,
organizationId: string
): Promise<BranchSeedRow[]> {
const branchRows: BranchSeedRow[] = [];
for (const [category, options] of Object.entries(FOUNDATION_OPTIONS) as Array<
[string, SeedOption[]]
>) {
for (const option of options) {
const [row] = await sql`
insert into ms_options (
id,
organization_id,
category,
code,
label,
value,
parent_id,
sort_order,
is_active,
metadata,
deleted_at
) values (
${crypto.randomUUID()},
${organizationId},
${category},
${option.code},
${option.label},
${option.value},
${null},
${option.sortOrder},
${true},
${null},
${null}
)
on conflict (organization_id, category, code) do update
set
label = excluded.label,
value = excluded.value,
parent_id = excluded.parent_id,
sort_order = excluded.sort_order,
is_active = excluded.is_active,
metadata = excluded.metadata,
deleted_at = excluded.deleted_at,
updated_at = now()
returning id, category, code, label
`;
if (category === 'crm_branch') {
branchRows.push(row);
}
}
}
return branchRows;
}
async function upsertDocumentSequencesForOrganization(
sql: SqlClient,
organizationId: string,
branchRows: BranchSeedRow[]
) {
if (!branchRows.length) {
throw new Error(
`No crm_branch options were found for organization ${organizationId}. Foundation branch seed must run first.`
);
}
const period = getCurrentPeriod();
for (const branch of branchRows) {
for (const sequence of DOCUMENT_SEQUENCES) {
await sql`
insert into document_sequences (
id,
organization_id,
branch_id,
document_type,
prefix,
period,
current_number,
padding_length,
is_active
) values (
${crypto.randomUUID()},
${organizationId},
${branch.id},
${sequence.documentType},
${sequence.prefix},
${period},
${0},
${3},
${true}
)
on conflict (organization_id, document_type, period, branch_id) do update
set
prefix = excluded.prefix,
padding_length = excluded.padding_length,
is_active = excluded.is_active,
updated_at = now()
`;
}
}
}
async function main() {
loadLocalEnv();
const databaseUrl = requireEnv('DATABASE_URL');
const sql = postgres(databaseUrl, { prepare: false });
try {
const organizations = await sql`
select id, name
from organizations
order by name asc
`;
if (organizations.length === 0) {
throw new Error(
'No organizations found. Create at least one organization before running foundation seed.'
);
}
for (const organization of organizations) {
const branchRows = await upsertMasterOptionsForOrganization(sql, organization.id);
await upsertDocumentSequencesForOrganization(sql, organization.id, branchRows);
console.log(`[seed-foundation] Seeded foundation data for ${organization.name}`);
}
} finally {
await sql.end();
}
}
if (require.main === module) {
main().catch((error) => {
console.error('[seed-foundation] Failed:', error.message);
process.exitCode = 1;
});
}