'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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { activateDocumentTemplateVersionMutation, archiveDocumentTemplateVersionMutation, compareDocumentTemplateVersionsMutation, duplicateDocumentTemplateVersionMutation, previewDocumentTemplateVersionMutation, publishDocumentTemplateVersionMutation, rollbackDocumentTemplateVersionMutation, validateDocumentTemplateVersionMutation, } from '../mutations'; import { documentTemplateVersionManagementQueryOptions } from '../queries'; import type { DocumentTemplateVersionAuditSummary, DocumentTemplateVersionCompareResponse, DocumentTemplateVersionDetail, DocumentTemplateVersionValidationSummary, DocumentTemplateVersionVisualSummary, } from '../types'; function getStatusBadgeVariant(status: 'PASS' | 'WARNING' | 'FAIL' | 'NOT_RUN') { if (status === 'PASS') { return 'secondary' as const; } if (status === 'FAIL') { return 'destructive' as const; } return 'outline' as const; } function getLifecycleBadgeVariant( status: DocumentTemplateVersionDetail['lifecycleStatus'], ) { if (status === 'active') { return 'secondary' as const; } if (status === 'archived') { return 'outline' as const; } return 'default' as const; } function SummaryCard({ label, value, }: { label: string; value: React.ReactNode; }) { return (
{label}
{value}
); } function StringList({ title, items, }: { title: string; items: string[]; }) { if (!items.length) { return null; } return (
{title}
{items.map((item) => (
{item}
))}
); } function ValidationIssues({ validation, }: { validation: DocumentTemplateVersionValidationSummary | null | undefined; }) { if (!validation?.issues.length) { return null; } return (
Template Health Issues
{validation.issues.map((issue) => (
{issue.severity} {issue.code} {issue.location}
{issue.message}
{issue.suggestedFix ? (
{issue.suggestedFix}
) : null}
))}
); } function AuditSummary({ audit, visual, }: { audit: DocumentTemplateVersionAuditSummary | null; visual: DocumentTemplateVersionVisualSummary | null | undefined; }) { if (!audit) { return (
No audit summary available.
); } return (
{audit.status}} /> {audit.visualRegressionStatus} } />
); } function CompareDialog({ open, onOpenChange, templateId, currentVersion, versions, }: { open: boolean; onOpenChange: (open: boolean) => void; templateId: string; currentVersion: DocumentTemplateVersionDetail; versions: DocumentTemplateVersionDetail[]; }) { const [rightVersionId, setRightVersionId] = React.useState( versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id, ); const [comparison, setComparison] = React.useState< DocumentTemplateVersionCompareResponse['comparison'] | null >(null); const compareMutation = useMutation({ ...compareDocumentTemplateVersionsMutation, onError: (error) => toast.error(error instanceof Error ? error.message : 'Compare failed'), }); React.useEffect(() => { if (open) { setComparison(null); setRightVersionId( versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id, ); } }, [currentVersion.id, open, versions]); async function handleCompare() { const result = await compareMutation.mutateAsync({ templateId, leftVersionId: currentVersion.id, rightVersionId, }); setComparison(result.comparison); } return ( Compare Versions Review metadata, placeholders, mappings, schema fields, and health state without opening raw JSON manually.
Base Version
{currentVersion.version}
Compare With
{comparison ? ( Metadata Placeholders Mappings Schema Health {comparison.metadataChanges.length ? ( comparison.metadataChanges.map((change) => (
{change.field}
Left: {String(change.left ?? 'null')}
Right: {String(change.right ?? 'null')}
)) ) : (
No metadata differences detected.
)}
{!comparison.placeholderOnlyLeft.length && !comparison.placeholderOnlyRight.length ? (
Placeholder sets are identical.
) : null}
{comparison.mappingDifferences.length ? ( comparison.mappingDifferences.map((difference) => (
{difference.placeholderKey}
{difference.difference}
)) ) : (
No mapping differences detected.
)}
{comparison.fieldSchemaChanges.changedFields.length ? (
Changed Field Types
{comparison.fieldSchemaChanges.changedFields.map((item) => (
{item.field}: {item.difference}
))}
) : null} {comparison.jsonDiff.length ? (
JSON Differences
{comparison.jsonDiff.map((item) => (
{item.path}: {String(item.left ?? 'null')} {'->'}{' '} {String(item.right ?? 'null')}
))}
) : null}
) : null}
); } export function TemplateVersionManagementPanel({ templateId, version, versions, }: { templateId: string; version: DocumentTemplateVersionDetail; versions: DocumentTemplateVersionDetail[]; }) { const { data } = useSuspenseQuery( documentTemplateVersionManagementQueryOptions(version.id), ); const managedVersion = data.version; const [compareOpen, setCompareOpen] = React.useState(false); const validateMutation = useMutation({ ...validateDocumentTemplateVersionMutation, onSuccess: (result) => toast.success(`Validation: ${result.validation.status}`), onError: (error) => toast.error(error instanceof Error ? error.message : 'Validate failed'), }); const publishMutation = useMutation({ ...publishDocumentTemplateVersionMutation, onSuccess: () => toast.success('Version published'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Publish failed'), }); const activateMutation = useMutation({ ...activateDocumentTemplateVersionMutation, onSuccess: () => toast.success('Version activated'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Activate failed'), }); const rollbackMutation = useMutation({ ...rollbackDocumentTemplateVersionMutation, onSuccess: () => toast.success('Rollback completed'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Rollback failed'), }); const archiveMutation = useMutation({ ...archiveDocumentTemplateVersionMutation, onSuccess: () => toast.success('Version archived'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Archive failed'), }); const duplicateMutation = useMutation({ ...duplicateDocumentTemplateVersionMutation, onSuccess: () => toast.success('Draft duplicate created'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Duplicate failed'), }); const previewMutation = useMutation({ ...previewDocumentTemplateVersionMutation, onSuccess: (result) => { const url = `data:${result.preview.contentType};base64,${result.preview.base64}`; window.open(url, '_blank', 'noopener,noreferrer'); toast.success(`Preview ready (${result.preview.pageCount} pages)`); }, onError: (error) => toast.error(error instanceof Error ? error.message : 'Preview failed'), }); const validation = managedVersion.validationSummary ?? null; const audit = managedVersion.auditSummary ?? null; const visual = managedVersion.visualSummary ?? null; return ( <>
Version Lifecycle Management
{managedVersion.lifecycleStatus ?? 'draft'} {managedVersion.isActive ? 'Active Runtime' : 'Inactive Runtime'} {validation ? ( Validation {validation.status} ) : null}
Validation Summary
{validation ? (
{validation.status}} />
) : (
Validation has not been run for this version yet.
)}
Audit Summary
); }