task-g
This commit is contained in:
101
src/db/schema.ts
101
src/db/schema.ts
@@ -465,3 +465,104 @@ export const crmApprovalActions = pgTable('crm_approval_actions', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
||||
});
|
||||
|
||||
export const crmDocumentTemplates = pgTable(
|
||||
'crm_document_templates',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
documentType: text('document_type').notNull(),
|
||||
productType: text('product_type').notNull(),
|
||||
fileType: text('file_type').notNull(),
|
||||
templateName: text('template_name').notNull(),
|
||||
description: text('description'),
|
||||
isDefault: boolean('is_default').default(false).notNull(),
|
||||
isActive: boolean('is_active').default(true).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||
createdBy: text('created_by').notNull(),
|
||||
updatedBy: text('updated_by').notNull()
|
||||
},
|
||||
(table) => ({
|
||||
organizationTemplateIdx: uniqueIndex('crm_document_templates_org_doc_product_file_name_idx').on(
|
||||
table.organizationId,
|
||||
table.documentType,
|
||||
table.productType,
|
||||
table.fileType,
|
||||
table.templateName
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
export const crmDocumentTemplateVersions = pgTable(
|
||||
'crm_document_template_versions',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
templateId: text('template_id').notNull(),
|
||||
version: text('version').notNull(),
|
||||
filePath: text('file_path'),
|
||||
schemaJson: jsonb('schema_json').notNull(),
|
||||
previewImageUrl: text('preview_image_url'),
|
||||
isActive: boolean('is_active').default(true).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||
createdBy: text('created_by').notNull()
|
||||
},
|
||||
(table) => ({
|
||||
templateVersionIdx: uniqueIndex('crm_document_template_versions_template_version_idx').on(
|
||||
table.templateId,
|
||||
table.version
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
export const crmDocumentTemplateMappings = pgTable(
|
||||
'crm_document_template_mappings',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
templateVersionId: text('template_version_id').notNull(),
|
||||
placeholderKey: text('placeholder_key').notNull(),
|
||||
sourcePath: text('source_path').notNull(),
|
||||
dataType: text('data_type').notNull(),
|
||||
sheetName: text('sheet_name'),
|
||||
defaultValue: text('default_value'),
|
||||
formatMask: text('format_mask'),
|
||||
sortOrder: integer('sort_order').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
||||
},
|
||||
(table) => ({
|
||||
templatePlaceholderIdx: uniqueIndex('crm_document_template_mappings_version_placeholder_idx').on(
|
||||
table.templateVersionId,
|
||||
table.placeholderKey
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
export const crmDocumentTemplateTableColumns = pgTable(
|
||||
'crm_document_template_table_columns',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
mappingId: text('mapping_id').notNull(),
|
||||
columnName: text('column_name').notNull(),
|
||||
sourceField: text('source_field').notNull(),
|
||||
columnLetter: text('column_letter'),
|
||||
sortOrder: integer('sort_order').default(0).notNull(),
|
||||
formatMask: text('format_mask'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
||||
},
|
||||
(table) => ({
|
||||
mappingColumnIdx: uniqueIndex('crm_document_template_table_columns_mapping_name_idx').on(
|
||||
table.mappingId,
|
||||
table.columnName
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
@@ -18,6 +18,24 @@ type BranchSeedRow = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type TemplateColumnSeed = {
|
||||
columnName: string;
|
||||
sourceField: string;
|
||||
columnLetter?: string;
|
||||
sortOrder: number;
|
||||
formatMask?: string | null;
|
||||
};
|
||||
|
||||
type TemplateMappingSeed = {
|
||||
placeholderKey: string;
|
||||
sourcePath: string;
|
||||
dataType: 'scalar' | 'multiline' | 'table' | 'image';
|
||||
defaultValue?: string | null;
|
||||
formatMask?: string | null;
|
||||
sortOrder: number;
|
||||
columns?: TemplateColumnSeed[];
|
||||
};
|
||||
|
||||
function loadEnvFile(filePath: string) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return;
|
||||
@@ -213,6 +231,145 @@ const APPROVAL_WORKFLOWS = [
|
||||
}
|
||||
];
|
||||
|
||||
const DOCUMENT_TEMPLATE_MAPPINGS: TemplateMappingSeed[] = [
|
||||
{
|
||||
placeholderKey: 'customer_name',
|
||||
sourcePath: 'customer.name',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 1
|
||||
},
|
||||
{
|
||||
placeholderKey: 'customer_addr',
|
||||
sourcePath: 'customer.address',
|
||||
dataType: 'multiline',
|
||||
sortOrder: 2
|
||||
},
|
||||
{
|
||||
placeholderKey: 'customer_tel',
|
||||
sourcePath: 'customer.phone',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 3
|
||||
},
|
||||
{
|
||||
placeholderKey: 'customer_email',
|
||||
sourcePath: 'customer.email',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 4
|
||||
},
|
||||
{
|
||||
placeholderKey: 'customer_att',
|
||||
sourcePath: 'contact.name',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 5
|
||||
},
|
||||
{
|
||||
placeholderKey: 'project_name',
|
||||
sourcePath: 'quotation.projectName',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 6
|
||||
},
|
||||
{
|
||||
placeholderKey: 'site_location',
|
||||
sourcePath: 'quotation.projectLocation',
|
||||
dataType: 'multiline',
|
||||
sortOrder: 7
|
||||
},
|
||||
{
|
||||
placeholderKey: 'quotation_date',
|
||||
sourcePath: 'quotation.quotationDate',
|
||||
dataType: 'scalar',
|
||||
formatMask: 'date',
|
||||
sortOrder: 8
|
||||
},
|
||||
{
|
||||
placeholderKey: 'quotation_code',
|
||||
sourcePath: 'quotation.code',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 9
|
||||
},
|
||||
{
|
||||
placeholderKey: 'quotation_price',
|
||||
sourcePath: 'quotation.totalAmount',
|
||||
dataType: 'scalar',
|
||||
formatMask: 'currency',
|
||||
sortOrder: 10
|
||||
},
|
||||
{
|
||||
placeholderKey: 'currency',
|
||||
sourcePath: 'quotation.currencyCode',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 11
|
||||
},
|
||||
{
|
||||
placeholderKey: 'exclusion_data',
|
||||
sourcePath: 'topics.exclusion',
|
||||
dataType: 'multiline',
|
||||
sortOrder: 12
|
||||
},
|
||||
{
|
||||
placeholderKey: 'item_topic',
|
||||
sourcePath: 'topics.scope',
|
||||
dataType: 'multiline',
|
||||
sortOrder: 13
|
||||
},
|
||||
{
|
||||
placeholderKey: 'items_table',
|
||||
sourcePath: 'items',
|
||||
dataType: 'table',
|
||||
sortOrder: 14,
|
||||
columns: [
|
||||
{ columnName: 'Item', sourceField: 'itemNumber', columnLetter: 'A', sortOrder: 1 },
|
||||
{ columnName: 'Description', sourceField: 'description', columnLetter: 'B', sortOrder: 2 },
|
||||
{ columnName: 'Qty', sourceField: 'quantity', columnLetter: 'C', sortOrder: 3 },
|
||||
{ columnName: 'Unit', sourceField: 'unitLabel', columnLetter: 'D', sortOrder: 4 },
|
||||
{
|
||||
columnName: 'Unit Price',
|
||||
sourceField: 'unitPrice',
|
||||
columnLetter: 'E',
|
||||
sortOrder: 5,
|
||||
formatMask: 'currency'
|
||||
},
|
||||
{
|
||||
columnName: 'Total',
|
||||
sourceField: 'totalPrice',
|
||||
columnLetter: 'F',
|
||||
sortOrder: 6,
|
||||
formatMask: 'currency'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function loadTemplateSchemaByOrganizationName(organizationName: string) {
|
||||
const normalized = organizationName.toUpperCase();
|
||||
const fileName = normalized.includes('ONVALLA')
|
||||
? 'ONVALLA_template_pdfme_fainal3.json'
|
||||
: normalized.includes('ALLA')
|
||||
? 'ALLA_template_pdfme_fainal3.json'
|
||||
: null;
|
||||
|
||||
if (!fileName) {
|
||||
return {
|
||||
filePath: null,
|
||||
templateName: `${organizationName} Quotation Standard`,
|
||||
schemaJson: {
|
||||
basePdf: '',
|
||||
schemas: [],
|
||||
pdfmeVersion: 'fallback'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(process.cwd(), 'src', 'pdfme_template', fileName);
|
||||
const raw = fs.readFileSync(absolutePath, 'utf8');
|
||||
|
||||
return {
|
||||
filePath: `src/pdfme_template/${fileName}`,
|
||||
templateName: `${organizationName} Quotation Standard`,
|
||||
schemaJson: JSON.parse(raw)
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertMasterOptionsForOrganization(
|
||||
sql: SqlClient,
|
||||
organizationId: string
|
||||
@@ -382,6 +539,188 @@ async function upsertApprovalWorkflowsForOrganization(sql: SqlClient, organizati
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertDocumentTemplatesForOrganization(
|
||||
sql: SqlClient,
|
||||
organization: { id: string; name: string; createdBy: string }
|
||||
) {
|
||||
const templateSeed = loadTemplateSchemaByOrganizationName(organization.name);
|
||||
const [templateRow] = await sql`
|
||||
insert into crm_document_templates (
|
||||
id,
|
||||
organization_id,
|
||||
document_type,
|
||||
product_type,
|
||||
file_type,
|
||||
template_name,
|
||||
description,
|
||||
is_default,
|
||||
is_active,
|
||||
deleted_at,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${'quotation'},
|
||||
${'default'},
|
||||
${'pdfme'},
|
||||
${templateSeed.templateName},
|
||||
${'Seeded quotation template metadata for production preview foundation.'},
|
||||
${true},
|
||||
${true},
|
||||
${null},
|
||||
${organization.createdBy},
|
||||
${organization.createdBy}
|
||||
)
|
||||
on conflict do nothing
|
||||
returning id
|
||||
`;
|
||||
|
||||
const resolvedTemplateId =
|
||||
templateRow?.id ??
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_document_templates
|
||||
where organization_id = ${organization.id}
|
||||
and document_type = ${'quotation'}
|
||||
and product_type = ${'default'}
|
||||
and file_type = ${'pdfme'}
|
||||
and deleted_at is null
|
||||
order by is_default desc, created_at asc
|
||||
limit 1
|
||||
`
|
||||
)[0]?.id;
|
||||
|
||||
if (!resolvedTemplateId) {
|
||||
throw new Error(`Unable to resolve template id for organization ${organization.id}`);
|
||||
}
|
||||
|
||||
const [versionRow] = await sql`
|
||||
insert into crm_document_template_versions (
|
||||
id,
|
||||
organization_id,
|
||||
template_id,
|
||||
version,
|
||||
file_path,
|
||||
schema_json,
|
||||
preview_image_url,
|
||||
is_active,
|
||||
deleted_at,
|
||||
created_by
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${resolvedTemplateId},
|
||||
${'1.0'},
|
||||
${templateSeed.filePath},
|
||||
${JSON.stringify(templateSeed.schemaJson)},
|
||||
${null},
|
||||
${true},
|
||||
${null},
|
||||
${organization.createdBy}
|
||||
)
|
||||
on conflict do nothing
|
||||
returning id
|
||||
`;
|
||||
|
||||
const resolvedVersionId =
|
||||
versionRow?.id ??
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_document_template_versions
|
||||
where organization_id = ${organization.id}
|
||||
and template_id = ${resolvedTemplateId}
|
||||
and version = ${'1.0'}
|
||||
and deleted_at is null
|
||||
order by created_at asc
|
||||
limit 1
|
||||
`
|
||||
)[0]?.id;
|
||||
|
||||
if (!resolvedVersionId) {
|
||||
throw new Error(`Unable to resolve template version id for organization ${organization.id}`);
|
||||
}
|
||||
|
||||
for (const mapping of DOCUMENT_TEMPLATE_MAPPINGS) {
|
||||
const [mappingRow] = await sql`
|
||||
insert into crm_document_template_mappings (
|
||||
id,
|
||||
organization_id,
|
||||
template_version_id,
|
||||
placeholder_key,
|
||||
source_path,
|
||||
data_type,
|
||||
sheet_name,
|
||||
default_value,
|
||||
format_mask,
|
||||
sort_order,
|
||||
deleted_at
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${resolvedVersionId},
|
||||
${mapping.placeholderKey},
|
||||
${mapping.sourcePath},
|
||||
${mapping.dataType},
|
||||
${null},
|
||||
${mapping.defaultValue ?? null},
|
||||
${mapping.formatMask ?? null},
|
||||
${mapping.sortOrder},
|
||||
${null}
|
||||
)
|
||||
on conflict do nothing
|
||||
returning id
|
||||
`;
|
||||
|
||||
const resolvedMappingId =
|
||||
mappingRow?.id ??
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_document_template_mappings
|
||||
where organization_id = ${organization.id}
|
||||
and template_version_id = ${resolvedVersionId}
|
||||
and placeholder_key = ${mapping.placeholderKey}
|
||||
and deleted_at is null
|
||||
limit 1
|
||||
`
|
||||
)[0]?.id;
|
||||
|
||||
if (!resolvedMappingId || !mapping.columns?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const column of mapping.columns) {
|
||||
await sql`
|
||||
insert into crm_document_template_table_columns (
|
||||
id,
|
||||
organization_id,
|
||||
mapping_id,
|
||||
column_name,
|
||||
source_field,
|
||||
column_letter,
|
||||
sort_order,
|
||||
format_mask,
|
||||
deleted_at
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${resolvedMappingId},
|
||||
${column.columnName},
|
||||
${column.sourceField},
|
||||
${column.columnLetter ?? null},
|
||||
${column.sortOrder},
|
||||
${column.formatMask ?? null},
|
||||
${null}
|
||||
)
|
||||
on conflict do nothing
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadLocalEnv();
|
||||
|
||||
@@ -390,7 +729,7 @@ async function main() {
|
||||
|
||||
try {
|
||||
const organizations = await sql`
|
||||
select id, name
|
||||
select id, name, created_by as "createdBy"
|
||||
from organizations
|
||||
order by name asc
|
||||
`;
|
||||
@@ -405,6 +744,7 @@ async function main() {
|
||||
const branchRows = await upsertMasterOptionsForOrganization(sql, organization.id);
|
||||
await upsertDocumentSequencesForOrganization(sql, organization.id, branchRows);
|
||||
await upsertApprovalWorkflowsForOrganization(sql, organization.id);
|
||||
await upsertDocumentTemplatesForOrganization(sql, organization);
|
||||
console.log(`[seed-foundation] Seeded foundation data for ${organization.name}`);
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user