'use client';
import * as React from 'react';
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Icons } from '@/components/icons';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { TemplateJsonSheet } from './template-json-sheet';
import { TemplateVersionManagementPanel } from './template-version-management-panel';
import {
createDocumentTemplateMappingMutation,
createDocumentTemplateMutation,
createDocumentTemplateVersionMutation,
deleteDocumentTemplateMappingMutation,
updateDocumentTemplateMappingMutation,
updateDocumentTemplateMutation,
updateDocumentTemplateVersionMutation
} from '../mutations';
import { documentTemplateByIdOptions, documentTemplatesQueryOptions } from '../queries';
import type {
DocumentTemplateDetail,
DocumentTemplateMappingMutationPayload,
DocumentTemplateMutationPayload,
DocumentTemplateVersionDetail,
DocumentTemplateVersionMutationPayload
} from '../types';
type TemplateFormState = {
documentType: string;
productType: string;
fileType: string;
templateName: string;
description: string;
isDefault: boolean;
isActive: boolean;
};
type VersionFormState = {
version: string;
filePath: string;
schemaJson: string;
previewImageUrl: string;
isActive: boolean;
};
type MappingColumnState = {
columnName: string;
sourceField: string;
columnLetter: string;
sortOrder: string;
formatMask: string;
};
type MappingFormState = {
placeholderKey: string;
sourcePath: string;
dataType: 'scalar' | 'multiline' | 'table' | 'image';
sheetName: string;
defaultValue: string;
formatMask: string;
sortOrder: string;
columns: MappingColumnState[];
};
function toTemplateState(template?: DocumentTemplateDetail): TemplateFormState {
return {
documentType: template?.documentType ?? 'quotation',
productType: template?.productType ?? 'default',
fileType: template?.fileType ?? 'pdfme',
templateName: template?.templateName ?? '',
description: template?.description ?? '',
isDefault: template?.isDefault ?? false,
isActive: template?.isActive ?? true
};
}
function toVersionState(version?: DocumentTemplateVersionDetail): VersionFormState {
return {
version: version?.version ?? '',
filePath: version?.filePath ?? '',
schemaJson: version ? JSON.stringify(version.schemaJson, null, 2) : '{}',
previewImageUrl: version?.previewImageUrl ?? '',
isActive: version?.isActive ?? true
};
}
function toMappingState(mapping?: DocumentTemplateVersionDetail['mappings'][number]): MappingFormState {
return {
placeholderKey: mapping?.placeholderKey ?? '',
sourcePath: mapping?.sourcePath ?? '',
dataType: mapping?.dataType ?? 'scalar',
sheetName: mapping?.sheetName ?? '',
defaultValue: mapping?.defaultValue ?? '',
formatMask: mapping?.formatMask ?? '',
sortOrder: String(mapping?.sortOrder ?? 0),
columns:
mapping?.columns.map((column) => ({
columnName: column.columnName,
sourceField: column.sourceField,
columnLetter: column.columnLetter ?? '',
sortOrder: String(column.sortOrder),
formatMask: column.formatMask ?? ''
})) ?? []
};
}
function buildTemplatePayload(state: TemplateFormState): DocumentTemplateMutationPayload {
return {
documentType: state.documentType,
productType: state.productType,
fileType: state.fileType,
templateName: state.templateName,
description: state.description || null,
isDefault: state.isDefault,
isActive: state.isActive
};
}
function buildVersionPayload(state: VersionFormState): DocumentTemplateVersionMutationPayload {
let schemaJson: unknown;
try {
schemaJson = JSON.parse(state.schemaJson);
} catch {
throw new Error('Schema JSON must be valid JSON');
}
return {
version: state.version,
filePath: state.filePath || null,
schemaJson,
previewImageUrl: state.previewImageUrl || null,
isActive: state.isActive
};
}
function buildMappingPayload(state: MappingFormState): DocumentTemplateMappingMutationPayload {
return {
placeholderKey: state.placeholderKey,
sourcePath: state.sourcePath,
dataType: state.dataType,
sheetName: state.sheetName || null,
defaultValue: state.defaultValue || null,
formatMask: state.formatMask || null,
sortOrder: Number(state.sortOrder || 0),
columns:
state.dataType === 'table'
? state.columns.map((column) => ({
columnName: column.columnName,
sourceField: column.sourceField,
columnLetter: column.columnLetter || null,
sortOrder: Number(column.sortOrder || 0),
formatMask: column.formatMask || null
}))
: []
};
}
function Field({
label,
children
}: {
label: string;
children: React.ReactNode;
}) {
return (
);
}
function getTableColumnCount(version: DocumentTemplateVersionDetail) {
return version.mappings.reduce((total, mapping) => total + mapping.columns.length, 0);
}
function TemplateDialog({
open,
onOpenChange,
template
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
template?: DocumentTemplateDetail;
}) {
const [state, setState] = React.useState(toTemplateState(template));
const createMutation = useMutation({
...createDocumentTemplateMutation,
onSuccess: () => {
toast.success('Template created');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Create failed')
});
const updateMutation = useMutation({
...updateDocumentTemplateMutation,
onSuccess: () => {
toast.success('Template updated');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Update failed')
});
React.useEffect(() => {
if (open) {
setState(toTemplateState(template));
}
}, [open, template]);
async function onSubmit(event: React.FormEvent) {
event.preventDefault();
const payload = buildTemplatePayload(state);
if (template) {
await updateMutation.mutateAsync({ id: template.id, values: payload });
return;
}
await createMutation.mutateAsync(payload);
}
const isPending = createMutation.isPending || updateMutation.isPending;
return (
);
}
function VersionSheet({
open,
onOpenChange,
templateId,
version
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
templateId: string;
version?: DocumentTemplateVersionDetail;
}) {
const [state, setState] = React.useState(toVersionState(version));
const createMutation = useMutation({
...createDocumentTemplateVersionMutation,
onSuccess: () => {
toast.success('Version saved');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Save failed')
});
const updateMutation = useMutation({
...updateDocumentTemplateVersionMutation,
onSuccess: () => {
toast.success('Version updated');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Update failed')
});
React.useEffect(() => {
if (open) {
setState(toVersionState(version));
}
}, [open, version]);
async function onSubmit(event: React.FormEvent) {
event.preventDefault();
const payload = buildVersionPayload(state);
if (version) {
await updateMutation.mutateAsync({ templateId, versionId: version.id, values: payload });
return;
}
await createMutation.mutateAsync({ id: templateId, values: payload });
}
const isPending = createMutation.isPending || updateMutation.isPending;
return (
{version ? 'Edit Version' : 'Create Version'}
Schema JSON is accepted as raw JSON in this admin release.
);
}
function MappingDialog({
open,
onOpenChange,
templateId,
version,
mapping
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
templateId: string;
version: DocumentTemplateVersionDetail;
mapping?: DocumentTemplateVersionDetail['mappings'][number];
}) {
const [state, setState] = React.useState(toMappingState(mapping));
const createMutation = useMutation({
...createDocumentTemplateMappingMutation,
onSuccess: () => {
toast.success('Mapping saved');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Save failed')
});
const updateMutation = useMutation({
...updateDocumentTemplateMappingMutation,
onSuccess: () => {
toast.success('Mapping updated');
onOpenChange(false);
},
onError: (error) => toast.error(error instanceof Error ? error.message : 'Update failed')
});
React.useEffect(() => {
if (open) {
setState(toMappingState(mapping));
}
}, [open, mapping]);
async function onSubmit(event: React.FormEvent) {
event.preventDefault();
const payload = buildMappingPayload(state);
if (mapping) {
await updateMutation.mutateAsync({ templateId, mappingId: mapping.id, values: payload });
return;
}
await createMutation.mutateAsync({ templateId, versionId: version.id, values: payload });
}
function updateColumn(index: number, field: keyof MappingColumnState, value: string) {
setState((current) => ({
...current,
columns: current.columns.map((column, columnIndex) =>
columnIndex === index ? { ...column, [field]: value } : column
)
}));
}
const isPending = createMutation.isPending || updateMutation.isPending;
return (
);
}
function TemplateDetailCard({ templateId }: { templateId: string }) {
const { data } = useSuspenseQuery(documentTemplateByIdOptions(templateId));
const template = data.template;
const [selectedVersionId, setSelectedVersionId] = React.useState(
template.versions[0]?.id
);
const [versionDialogState, setVersionDialogState] = React.useState<{
open: boolean;
version?: DocumentTemplateVersionDetail;
}>({ open: false });
const [mappingDialogState, setMappingDialogState] = React.useState<{
open: boolean;
version?: DocumentTemplateVersionDetail;
mapping?: DocumentTemplateVersionDetail['mappings'][number];
}>({ open: false });
const deleteMappingMutation = useMutation({
...deleteDocumentTemplateMappingMutation,
onSuccess: () => toast.success('Mapping deleted'),
onError: (error) => toast.error(error instanceof Error ? error.message : 'Delete failed')
});
React.useEffect(() => {
if (template.versions.length === 0) {
setSelectedVersionId(undefined);
return;
}
setSelectedVersionId((current) =>
current && template.versions.some((version) => version.id === current)
? current
: template.versions[0]?.id
);
}, [template.versions]);
return (
Document Type
{template.documentType}
Product Type
{template.productType}
Version Count
{template.versions.length}
Mapping Count
{template.mappingCount}
{template.versions.map((version) => (
{version.version}
))}
{template.versions.length === 0 ? (
No versions created yet.
) : null}
{template.versions.map((version) => (
Version {version.version}
File Path
{version.filePath || 'No file path set'}
Mappings
{version.mappings.length}
Table Columns
{getTableColumnCount(version)}
Preview Image
{version.previewImageUrl || '-'}
{version.isActive ? 'Active' : 'Inactive'}
{version.mappings.map((mapping) => (
{mapping.placeholderKey}
{mapping.sourcePath} • {mapping.dataType}
{mapping.columns.length ? (
Table Columns
{mapping.columns.map((column) => (
{column.columnName}
{column.sourceField}
{column.columnLetter ? ({column.columnLetter}) : null}
{column.formatMask ? • {column.formatMask} : null}
))}
) : null}
))}
{version.mappings.length === 0 ? (
No mappings defined for this version yet.
) : null}
))}
setVersionDialogState((current) => ({ ...current, open }))
}
templateId={template.id}
version={versionDialogState.version}
/>
{mappingDialogState.open && mappingDialogState.version ? (
setMappingDialogState((current) => ({ ...current, open }))}
templateId={template.id}
version={mappingDialogState.version}
mapping={mappingDialogState.mapping}
/>
) : null}
);
}
export function TemplateSettings() {
const { data } = useSuspenseQuery(
documentTemplatesQueryOptions({ documentType: 'quotation', fileType: 'pdfme' })
);
const [templateDialogState, setTemplateDialogState] = React.useState<{
open: boolean;
templateId?: string;
}>({ open: false });
return (
Template Configuration Center
Admins can manage metadata, versions, mappings, and JSON schema without a visual designer.
{!data.items.length ? (
No production document templates found.
) : (
data.items.map((template) => (
{template.templateName}
{template.description || 'No description provided.'}
{template.isDefault ? Default : null}
{template.isActive ? 'Active' : 'Inactive'}
{template.fileType}
Document Type
{template.documentType}
Product Type
{template.productType}
Latest Version
{template.latestVersion ?? '-'}
Version Count
{template.versionCount}
Mapping Count
{template.mappingCount}
Loading template detail...
}
>
))
)}
setTemplateDialogState((current) => ({ ...current, open }))}
/>
{templateDialogState.open && templateDialogState.templateId ? (
setTemplateDialogState((current) => ({ ...current, open }))}
/>
) : null}
);
}
function TemplateDetailDialog({
templateId,
open,
onOpenChange
}: {
templateId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { data } = useSuspenseQuery(documentTemplateByIdOptions(templateId));
return ;
}