This commit is contained in:
phaichayon
2026-06-16 10:57:41 +07:00
parent dff22d75b5
commit 357414c247
33 changed files with 9281 additions and 26 deletions

View File

@@ -0,0 +1,62 @@
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { documentTemplatesQueryOptions } from '../queries';
export function TemplateSettings() {
const { data } = useSuspenseQuery(
documentTemplatesQueryOptions({ documentType: 'quotation', fileType: 'pdfme' })
);
return (
<div className='space-y-6'>
{!data.items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No production document templates found.
</div>
) : (
data.items.map((template) => (
<Card key={template.id}>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div className='space-y-1'>
<CardTitle>{template.templateName}</CardTitle>
<CardDescription>{template.description || 'No description provided.'}</CardDescription>
</div>
<div className='flex flex-wrap gap-2'>
{template.isDefault ? <Badge>Default</Badge> : null}
<Badge variant={template.isActive ? 'secondary' : 'outline'}>
{template.isActive ? 'Active' : 'Inactive'}
</Badge>
<Badge variant='outline'>{template.fileType}</Badge>
</div>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-5'>
<div>
<div className='text-muted-foreground text-xs'>Document Type</div>
<div className='text-sm font-medium'>{template.documentType}</div>
</div>
<div>
<div className='text-muted-foreground text-xs'>Product Type</div>
<div className='text-sm font-medium'>{template.productType}</div>
</div>
<div>
<div className='text-muted-foreground text-xs'>Latest Version</div>
<div className='text-sm font-medium'>{template.latestVersion ?? '-'}</div>
</div>
<div>
<div className='text-muted-foreground text-xs'>Version Count</div>
<div className='text-sm font-medium'>{template.versionCount}</div>
</div>
<div>
<div className='text-muted-foreground text-xs'>Mapping Count</div>
<div className='text-sm font-medium'>{template.mappingCount}</div>
</div>
</CardContent>
</Card>
))
)}
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { mutationOptions } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/query-client';
import {
createDocumentTemplate,
createDocumentTemplateVersion,
deleteDocumentTemplate,
updateDocumentTemplate
} from './service';
import { documentTemplateKeys } from './queries';
import type { DocumentTemplateMutationPayload, DocumentTemplateVersionMutationPayload } from './types';
export const createDocumentTemplateMutation = mutationOptions({
mutationFn: (data: DocumentTemplateMutationPayload) => createDocumentTemplate(data),
onSuccess: () => {
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
}
});
export const updateDocumentTemplateMutation = mutationOptions({
mutationFn: ({ id, values }: { id: string; values: Partial<DocumentTemplateMutationPayload> }) =>
updateDocumentTemplate(id, values),
onSuccess: (_data, variables) => {
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) });
}
});
export const deleteDocumentTemplateMutation = mutationOptions({
mutationFn: (id: string) => deleteDocumentTemplate(id),
onSuccess: () => {
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
}
});
export const createDocumentTemplateVersionMutation = mutationOptions({
mutationFn: ({ id, values }: { id: string; values: DocumentTemplateVersionMutationPayload }) =>
createDocumentTemplateVersion(id, values),
onSuccess: (_data, variables) => {
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) });
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.versions(variables.id) });
}
});

View File

@@ -0,0 +1,29 @@
import { queryOptions } from '@tanstack/react-query';
import { getDocumentTemplateById, getDocumentTemplates, getDocumentTemplateVersions } from './service';
import type { DocumentTemplateFilters } from './types';
export const documentTemplateKeys = {
all: ['crm-document-templates'] as const,
list: (filters: DocumentTemplateFilters) =>
[...documentTemplateKeys.all, 'list', filters] as const,
detail: (id: string) => [...documentTemplateKeys.all, 'detail', id] as const,
versions: (id: string) => [...documentTemplateKeys.all, 'versions', id] as const
};
export const documentTemplatesQueryOptions = (filters: DocumentTemplateFilters = {}) =>
queryOptions({
queryKey: documentTemplateKeys.list(filters),
queryFn: () => getDocumentTemplates(filters)
});
export const documentTemplateByIdOptions = (id: string) =>
queryOptions({
queryKey: documentTemplateKeys.detail(id),
queryFn: () => getDocumentTemplateById(id)
});
export const documentTemplateVersionsQueryOptions = (id: string) =>
queryOptions({
queryKey: documentTemplateKeys.versions(id),
queryFn: () => getDocumentTemplateVersions(id)
});

View File

@@ -0,0 +1,563 @@
import { and, asc, eq, isNull } from 'drizzle-orm';
import {
crmDocumentTemplateMappings,
crmDocumentTemplateTableColumns,
crmDocumentTemplateVersions,
crmDocumentTemplates
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import type {
DocumentTemplateDetail,
DocumentTemplateFilters,
DocumentTemplateListItem,
DocumentTemplateMappingRecord,
DocumentTemplateMappingWithColumns,
DocumentTemplateMutationPayload,
DocumentTemplateRecord,
DocumentTemplateTableColumnRecord,
DocumentTemplateVersionDetail,
DocumentTemplateVersionMutationPayload,
DocumentTemplateVersionRecord,
ResolveTemplateParams,
ResolvedDocumentTemplate
} from '../types';
function mapTemplateRecord(row: typeof crmDocumentTemplates.$inferSelect): DocumentTemplateRecord {
return {
id: row.id,
organizationId: row.organizationId,
documentType: row.documentType,
productType: row.productType,
fileType: row.fileType,
templateName: row.templateName,
description: row.description,
isDefault: row.isDefault,
isActive: row.isActive,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
deletedAt: row.deletedAt?.toISOString() ?? null,
createdBy: row.createdBy,
updatedBy: row.updatedBy
};
}
function mapVersionRecord(
row: typeof crmDocumentTemplateVersions.$inferSelect
): DocumentTemplateVersionRecord {
return {
id: row.id,
organizationId: row.organizationId,
templateId: row.templateId,
version: row.version,
filePath: row.filePath,
schemaJson: row.schemaJson,
previewImageUrl: row.previewImageUrl,
isActive: row.isActive,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
deletedAt: row.deletedAt?.toISOString() ?? null,
createdBy: row.createdBy
};
}
function mapMappingRecord(
row: typeof crmDocumentTemplateMappings.$inferSelect
): DocumentTemplateMappingRecord {
return {
id: row.id,
organizationId: row.organizationId,
templateVersionId: row.templateVersionId,
placeholderKey: row.placeholderKey,
sourcePath: row.sourcePath,
dataType: row.dataType as DocumentTemplateMappingRecord['dataType'],
sheetName: row.sheetName,
defaultValue: row.defaultValue,
formatMask: row.formatMask,
sortOrder: row.sortOrder,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
deletedAt: row.deletedAt?.toISOString() ?? null
};
}
function mapTableColumnRecord(
row: typeof crmDocumentTemplateTableColumns.$inferSelect
): DocumentTemplateTableColumnRecord {
return {
id: row.id,
organizationId: row.organizationId,
mappingId: row.mappingId,
columnName: row.columnName,
sourceField: row.sourceField,
columnLetter: row.columnLetter,
sortOrder: row.sortOrder,
formatMask: row.formatMask,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
deletedAt: row.deletedAt?.toISOString() ?? null
};
}
async function assertTemplate(id: string, organizationId: string) {
const [template] = await db
.select()
.from(crmDocumentTemplates)
.where(
and(
eq(crmDocumentTemplates.id, id),
eq(crmDocumentTemplates.organizationId, organizationId),
isNull(crmDocumentTemplates.deletedAt)
)
)
.limit(1);
if (!template) {
throw new AuthError('Document template not found', 404);
}
return template;
}
async function listVersionsByTemplateIds(templateIds: string[], organizationId: string) {
if (!templateIds.length) {
return [];
}
return db
.select()
.from(crmDocumentTemplateVersions)
.where(
and(
eq(crmDocumentTemplateVersions.organizationId, organizationId),
isNull(crmDocumentTemplateVersions.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
}
export async function resolveTemplateMappings(
templateVersionId: string,
organizationId: string
): Promise<DocumentTemplateMappingWithColumns[]> {
const mappings = await db
.select()
.from(crmDocumentTemplateMappings)
.where(
and(
eq(crmDocumentTemplateMappings.templateVersionId, templateVersionId),
eq(crmDocumentTemplateMappings.organizationId, organizationId),
isNull(crmDocumentTemplateMappings.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateMappings.sortOrder), asc(crmDocumentTemplateMappings.createdAt));
const columns = await db
.select()
.from(crmDocumentTemplateTableColumns)
.where(
and(
eq(crmDocumentTemplateTableColumns.organizationId, organizationId),
isNull(crmDocumentTemplateTableColumns.deletedAt)
)
)
.orderBy(
asc(crmDocumentTemplateTableColumns.sortOrder),
asc(crmDocumentTemplateTableColumns.createdAt)
);
const columnMap = new Map<string, DocumentTemplateTableColumnRecord[]>();
for (const column of columns.map(mapTableColumnRecord)) {
const items = columnMap.get(column.mappingId) ?? [];
items.push(column);
columnMap.set(column.mappingId, items);
}
return mappings.map((mapping) => ({
...mapMappingRecord(mapping),
columns: columnMap.get(mapping.id) ?? []
}));
}
export async function listDocumentTemplates(
organizationId: string,
filters: DocumentTemplateFilters = {}
): Promise<DocumentTemplateListItem[]> {
const templates = await db
.select()
.from(crmDocumentTemplates)
.where(
and(
eq(crmDocumentTemplates.organizationId, organizationId),
isNull(crmDocumentTemplates.deletedAt),
...(filters.documentType ? [eq(crmDocumentTemplates.documentType, filters.documentType)] : []),
...(filters.fileType ? [eq(crmDocumentTemplates.fileType, filters.fileType)] : []),
...(filters.isActive === 'true' ? [eq(crmDocumentTemplates.isActive, true)] : []),
...(filters.isActive === 'false' ? [eq(crmDocumentTemplates.isActive, false)] : [])
)
)
.orderBy(asc(crmDocumentTemplates.templateName));
const versions = await db
.select()
.from(crmDocumentTemplateVersions)
.where(
and(
eq(crmDocumentTemplateVersions.organizationId, organizationId),
isNull(crmDocumentTemplateVersions.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
const mappings = await db
.select()
.from(crmDocumentTemplateMappings)
.where(
and(
eq(crmDocumentTemplateMappings.organizationId, organizationId),
isNull(crmDocumentTemplateMappings.deletedAt)
)
);
return templates.map((template) => {
const templateVersions = versions.filter((item) => item.templateId === template.id);
const latestVersion = templateVersions.at(-1) ?? null;
const templateVersionIds = new Set(templateVersions.map((item) => item.id));
const mappingCount = mappings.filter((item) => templateVersionIds.has(item.templateVersionId)).length;
return {
...mapTemplateRecord(template),
latestVersion: latestVersion?.version ?? null,
versionCount: templateVersions.length,
mappingCount
};
});
}
export async function getDocumentTemplate(
id: string,
organizationId: string
): Promise<DocumentTemplateDetail> {
const template = await assertTemplate(id, organizationId);
const versions = await db
.select()
.from(crmDocumentTemplateVersions)
.where(
and(
eq(crmDocumentTemplateVersions.templateId, id),
eq(crmDocumentTemplateVersions.organizationId, organizationId),
isNull(crmDocumentTemplateVersions.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
const versionsWithMappings: DocumentTemplateVersionDetail[] = [];
let mappingCount = 0;
for (const version of versions) {
const mappings = await resolveTemplateMappings(version.id, organizationId);
mappingCount += mappings.length;
versionsWithMappings.push({
...mapVersionRecord(version),
mappings
});
}
return {
...mapTemplateRecord(template),
versions: versionsWithMappings,
mappingCount
};
}
export async function listDocumentTemplateVersions(id: string, organizationId: string) {
await assertTemplate(id, organizationId);
const versions = await db
.select()
.from(crmDocumentTemplateVersions)
.where(
and(
eq(crmDocumentTemplateVersions.templateId, id),
eq(crmDocumentTemplateVersions.organizationId, organizationId),
isNull(crmDocumentTemplateVersions.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
return Promise.all(
versions.map(async (version) => ({
...mapVersionRecord(version),
mappings: await resolveTemplateMappings(version.id, organizationId)
}))
);
}
export async function createDocumentTemplate(
organizationId: string,
userId: string,
payload: DocumentTemplateMutationPayload
) {
const [created] = await db
.insert(crmDocumentTemplates)
.values({
id: crypto.randomUUID(),
organizationId,
documentType: payload.documentType,
productType: payload.productType,
fileType: payload.fileType,
templateName: payload.templateName.trim(),
description: payload.description?.trim() || null,
isDefault: payload.isDefault ?? false,
isActive: payload.isActive ?? true,
createdBy: userId,
updatedBy: userId
})
.returning();
if (created.isDefault) {
await normalizeDefaultTemplate(created.id, organizationId, created.documentType, created.fileType);
}
return created;
}
export async function updateDocumentTemplate(
id: string,
organizationId: string,
userId: string,
payload: Partial<DocumentTemplateMutationPayload>
) {
const template = await assertTemplate(id, organizationId);
const [updated] = await db
.update(crmDocumentTemplates)
.set({
documentType: payload.documentType ?? template.documentType,
productType: payload.productType ?? template.productType,
fileType: payload.fileType ?? template.fileType,
templateName: payload.templateName?.trim() ?? template.templateName,
description: payload.description === undefined ? template.description : payload.description?.trim() || null,
isDefault: payload.isDefault ?? template.isDefault,
isActive: payload.isActive ?? template.isActive,
updatedAt: new Date(),
updatedBy: userId
})
.where(eq(crmDocumentTemplates.id, id))
.returning();
if (updated.isDefault) {
await normalizeDefaultTemplate(updated.id, organizationId, updated.documentType, updated.fileType);
}
return updated;
}
async function normalizeDefaultTemplate(
templateId: string,
organizationId: string,
documentType: string,
fileType: string
) {
await db
.update(crmDocumentTemplates)
.set({
isDefault: false,
updatedAt: new Date()
})
.where(
and(
eq(crmDocumentTemplates.organizationId, organizationId),
eq(crmDocumentTemplates.documentType, documentType),
eq(crmDocumentTemplates.fileType, fileType),
isNull(crmDocumentTemplates.deletedAt)
)
);
await db
.update(crmDocumentTemplates)
.set({
isDefault: true,
updatedAt: new Date()
})
.where(eq(crmDocumentTemplates.id, templateId));
}
export async function softDeleteDocumentTemplate(id: string, organizationId: string, userId: string) {
await assertTemplate(id, organizationId);
const [updated] = await db
.update(crmDocumentTemplates)
.set({
deletedAt: new Date(),
updatedAt: new Date(),
updatedBy: userId
})
.where(eq(crmDocumentTemplates.id, id))
.returning();
return updated;
}
export async function createDocumentTemplateVersion(
templateId: string,
organizationId: string,
userId: string,
payload: DocumentTemplateVersionMutationPayload
) {
await assertTemplate(templateId, organizationId);
const [created] = await db
.insert(crmDocumentTemplateVersions)
.values({
id: crypto.randomUUID(),
organizationId,
templateId,
version: payload.version.trim(),
filePath: payload.filePath?.trim() || null,
schemaJson: payload.schemaJson,
previewImageUrl: payload.previewImageUrl?.trim() || null,
isActive: payload.isActive ?? true,
createdBy: userId
})
.returning();
return created;
}
export async function resolveTemplateForDocument(
organizationId: string,
params: ResolveTemplateParams
): Promise<ResolvedDocumentTemplate> {
const templates = await db
.select()
.from(crmDocumentTemplates)
.where(
and(
eq(crmDocumentTemplates.organizationId, organizationId),
eq(crmDocumentTemplates.documentType, params.documentType),
eq(crmDocumentTemplates.fileType, params.fileType),
isNull(crmDocumentTemplates.deletedAt),
eq(crmDocumentTemplates.isActive, true)
)
)
.orderBy(asc(crmDocumentTemplates.createdAt));
const exactTemplate =
templates.find(
(template) => template.productType === (params.productType ?? 'default') && template.isDefault
) ??
templates.find((template) => template.productType === (params.productType ?? 'default')) ??
templates.find((template) => template.productType === 'default' && template.isDefault) ??
templates.find((template) => template.productType === 'default') ??
templates.find((template) => template.isDefault) ??
templates[0];
if (!exactTemplate) {
throw new AuthError('No active document template found', 404);
}
const versions = await db
.select()
.from(crmDocumentTemplateVersions)
.where(
and(
eq(crmDocumentTemplateVersions.organizationId, organizationId),
eq(crmDocumentTemplateVersions.templateId, exactTemplate.id),
eq(crmDocumentTemplateVersions.isActive, true),
isNull(crmDocumentTemplateVersions.deletedAt)
)
)
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
const version = versions.at(-1);
if (!version) {
throw new AuthError('No active template version found', 404);
}
return {
template: mapTemplateRecord(exactTemplate),
version: mapVersionRecord(version)
};
}
function getValueByPath(source: unknown, path: string): unknown {
return path.split('.').reduce<unknown>((current, segment) => {
if (current === null || current === undefined) {
return undefined;
}
if (Array.isArray(current) && /^\d+$/.test(segment)) {
return current[Number(segment)];
}
if (typeof current === 'object' && current !== null && segment in current) {
return (current as Record<string, unknown>)[segment];
}
return undefined;
}, source);
}
function applyFormatMask(value: unknown, formatMask?: string | null) {
if (value === null || value === undefined) {
return value;
}
if (formatMask === 'currency' && typeof value === 'number') {
return value.toLocaleString();
}
if (formatMask === 'date' && typeof value === 'string') {
return new Date(value).toLocaleDateString();
}
return value;
}
export function mapDocumentDataToTemplateInput(
documentData: Record<string, unknown>,
mappings: DocumentTemplateMappingWithColumns[]
) {
const result: Record<string, unknown> = {};
for (const mapping of mappings) {
const rawValue = getValueByPath(documentData, mapping.sourcePath);
if (mapping.dataType === 'table') {
const rows = Array.isArray(rawValue) ? rawValue : [];
result[mapping.placeholderKey] = rows.map((row) => {
const rowRecord = row as Record<string, unknown>;
return mapping.columns.reduce<Record<string, unknown>>((accumulator, column) => {
accumulator[column.columnName] = applyFormatMask(
rowRecord[column.sourceField],
column.formatMask
);
return accumulator;
}, {});
});
continue;
}
if (mapping.dataType === 'multiline') {
if (Array.isArray(rawValue)) {
const normalized = rawValue
.map((item) => {
if (typeof item === 'string') {
return item;
}
if (item && typeof item === 'object' && 'items' in item) {
return ((item as { items?: string[] }).items ?? []).join('\n');
}
return String(item ?? '');
})
.filter(Boolean)
.join('\n\n');
result[mapping.placeholderKey] = normalized || mapping.defaultValue || '';
continue;
}
result[mapping.placeholderKey] =
typeof rawValue === 'string' ? rawValue : mapping.defaultValue || '';
continue;
}
result[mapping.placeholderKey] =
applyFormatMask(rawValue, mapping.formatMask) ?? mapping.defaultValue ?? '';
}
return result;
}

View File

@@ -0,0 +1,64 @@
import { apiClient } from '@/lib/api-client';
import type {
DocumentTemplateDetailResponse,
DocumentTemplateFilters,
DocumentTemplateListResponse,
DocumentTemplateMutationPayload,
DocumentTemplateVersionMutationPayload,
DocumentTemplateVersionsResponse,
MutationSuccessResponse
} from './types';
export async function getDocumentTemplates(filters: DocumentTemplateFilters = {}) {
const searchParams = new URLSearchParams();
if (filters.documentType) searchParams.set('documentType', filters.documentType);
if (filters.fileType) searchParams.set('fileType', filters.fileType);
if (filters.isActive) searchParams.set('isActive', filters.isActive);
const query = searchParams.toString();
return apiClient<DocumentTemplateListResponse>(
`/crm/document-templates${query ? `?${query}` : ''}`
);
}
export async function getDocumentTemplateById(id: string) {
return apiClient<DocumentTemplateDetailResponse>(`/crm/document-templates/${id}`);
}
export async function createDocumentTemplate(data: DocumentTemplateMutationPayload) {
return apiClient<MutationSuccessResponse>('/crm/document-templates', {
method: 'POST',
body: JSON.stringify(data)
});
}
export async function updateDocumentTemplate(
id: string,
data: Partial<DocumentTemplateMutationPayload>
) {
return apiClient<MutationSuccessResponse>(`/crm/document-templates/${id}`, {
method: 'PATCH',
body: JSON.stringify(data)
});
}
export async function deleteDocumentTemplate(id: string) {
return apiClient<MutationSuccessResponse>(`/crm/document-templates/${id}`, {
method: 'DELETE'
});
}
export async function getDocumentTemplateVersions(id: string) {
return apiClient<DocumentTemplateVersionsResponse>(`/crm/document-templates/${id}/versions`);
}
export async function createDocumentTemplateVersion(
id: string,
data: DocumentTemplateVersionMutationPayload
) {
return apiClient<MutationSuccessResponse>(`/crm/document-templates/${id}/versions`, {
method: 'POST',
body: JSON.stringify(data)
});
}

View File

@@ -0,0 +1,141 @@
export interface DocumentTemplateRecord {
id: string;
organizationId: string;
documentType: string;
productType: string;
fileType: string;
templateName: string;
description: string | null;
isDefault: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
createdBy: string;
updatedBy: string;
}
export interface DocumentTemplateVersionRecord {
id: string;
organizationId: string;
templateId: string;
version: string;
filePath: string | null;
schemaJson: unknown;
previewImageUrl: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
createdBy: string;
}
export interface DocumentTemplateMappingRecord {
id: string;
organizationId: string;
templateVersionId: string;
placeholderKey: string;
sourcePath: string;
dataType: 'scalar' | 'multiline' | 'table' | 'image';
sheetName: string | null;
defaultValue: string | null;
formatMask: string | null;
sortOrder: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface DocumentTemplateTableColumnRecord {
id: string;
organizationId: string;
mappingId: string;
columnName: string;
sourceField: string;
columnLetter: string | null;
sortOrder: number;
formatMask: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface DocumentTemplateMappingWithColumns extends DocumentTemplateMappingRecord {
columns: DocumentTemplateTableColumnRecord[];
}
export interface DocumentTemplateVersionDetail extends DocumentTemplateVersionRecord {
mappings: DocumentTemplateMappingWithColumns[];
}
export interface DocumentTemplateDetail extends DocumentTemplateRecord {
versions: DocumentTemplateVersionDetail[];
mappingCount: number;
}
export interface DocumentTemplateListItem extends DocumentTemplateRecord {
latestVersion: string | null;
versionCount: number;
mappingCount: number;
}
export interface DocumentTemplateFilters {
documentType?: string;
fileType?: string;
isActive?: string;
}
export interface DocumentTemplateListResponse {
success: boolean;
time: string;
message: string;
items: DocumentTemplateListItem[];
}
export interface DocumentTemplateDetailResponse {
success: boolean;
time: string;
message: string;
template: DocumentTemplateDetail;
}
export interface DocumentTemplateVersionsResponse {
success: boolean;
time: string;
message: string;
items: DocumentTemplateVersionDetail[];
}
export interface DocumentTemplateMutationPayload {
documentType: string;
productType: string;
fileType: string;
templateName: string;
description?: string | null;
isDefault?: boolean;
isActive?: boolean;
}
export interface DocumentTemplateVersionMutationPayload {
version: string;
filePath?: string | null;
schemaJson: unknown;
previewImageUrl?: string | null;
isActive?: boolean;
}
export interface ResolveTemplateParams {
documentType: string;
productType?: string | null;
fileType: string;
}
export interface ResolvedDocumentTemplate {
template: DocumentTemplateRecord;
version: DocumentTemplateVersionRecord;
}
export interface MutationSuccessResponse {
success: boolean;
message: string;
}