'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { Dialog, Transition } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import useContractManagement, { DocumentTemplate } from '../hooks/useContractManagement'; import ConfirmActionModal from '../../../components/modals/ConfirmActionModal'; import { useTranslation } from '../../../i18n/useTranslation'; type Props = { refreshKey?: number; onEdit?: (id: string) => void; }; type ContractTemplate = { id: string; name: string; type?: string; contract_type?: string | null; user_type?: string | null; lang?: string | null; version: number; status: 'draft' | 'published' | 'archived' | string; updatedAt?: string; description?: string; }; type TemplateFamilyKey = 'contract' | 'invoice' | 'other'; type ContractTypeKey = 'contract' | 'gdpr' | 'abo'; type AudienceKey = 'personal' | 'company' | 'both'; type TemplateLanguageColumn = { key: string; label: string; templates: ContractTemplate[]; activeTemplate?: ContractTemplate; }; type TemplateTrack = { key: string; family: TemplateFamilyKey; title: string; subtitle: string; description?: string; templates: ContractTemplate[]; languageColumns: TemplateLanguageColumn[]; activeCount: number; latestUpdatedAt?: string; }; type TemplateFamilyGroup = { key: TemplateFamilyKey; label: string; description: string; tracks: TemplateTrack[]; totalTemplates: number; activeTemplates: number; }; type ContractTypeMeta = { label: string; shortLabel: string; description: string; badgeClass: string; headerClass: string; iconClass: string; sectionClass: string; trackClass: string; }; type ContractTypeSection = { key: ContractTypeKey; meta: ContractTypeMeta; tracks: TemplateTrack[]; totalTemplates: number; activeTemplates: number; }; type VersionHistoryModalState = { trackTitle: string; languageLabel: string; templates: ContractTemplate[]; }; type NormalizedTemplate = DocumentTemplate & { _id?: string; uuid?: string; description?: string; modifiedAt?: string; updated_at?: string; contractType?: string | null; userType?: string | null; }; const FAMILY_ORDER: TemplateFamilyKey[] = ['contract', 'invoice', 'other']; const FAMILY_META: Record = { contract: { label: 'Contracts', description: 'ABO, GDPR and standard contracts grouped by audience and language.', tabClass: 'border-amber-200 bg-white text-slate-700 hover:border-amber-300 hover:bg-amber-50/80', tabActiveClass: 'border-amber-300 bg-amber-50 text-amber-950 shadow-[0_12px_30px_-18px_rgba(217,119,6,0.65)]', tagClass: 'border-amber-200 bg-amber-50 text-amber-900', icon: 'C', }, invoice: { label: 'Invoices', description: 'Invoice layouts with their active language variants and revision history.', tabClass: 'border-sky-200 bg-white text-slate-700 hover:border-sky-300 hover:bg-sky-50/80', tabActiveClass: 'border-sky-300 bg-sky-50 text-sky-950 shadow-[0_12px_30px_-18px_rgba(2,132,199,0.55)]', tagClass: 'border-sky-200 bg-sky-50 text-sky-900', icon: 'I', }, other: { label: 'Other', description: 'Custom templates that do not belong to the main contract or invoice flows.', tabClass: 'border-emerald-200 bg-white text-slate-700 hover:border-emerald-300 hover:bg-emerald-50/80', tabActiveClass: 'border-emerald-300 bg-emerald-50 text-emerald-950 shadow-[0_12px_30px_-18px_rgba(5,150,105,0.55)]', tagClass: 'border-emerald-200 bg-emerald-50 text-emerald-900', icon: 'O', }, }; const CONTRACT_TYPE_ORDER: ContractTypeKey[] = ['abo', 'gdpr', 'contract']; const CONTRACT_TYPE_META: Record = { abo: { label: 'ABO Contracts', shortLabel: 'ABO', description: 'Subscription and recurring-delivery templates with their own active revision lanes.', badgeClass: 'border-amber-200 bg-amber-50 text-amber-900', headerClass: 'border-amber-200 bg-amber-50 text-amber-950', iconClass: 'border-amber-200 bg-amber-100 text-amber-950', sectionClass: 'border-amber-200 bg-[linear-gradient(180deg,rgba(255,251,235,0.96)_0%,rgba(255,255,255,0.99)_24%)]', trackClass: 'border-amber-200 bg-[linear-gradient(180deg,rgba(255,251,235,0.92)_0%,rgba(255,255,255,0.99)_28%)]', }, gdpr: { label: 'GDPR Contracts', shortLabel: 'GDPR', description: 'Privacy and consent-related templates separated from commercial contract flows.', badgeClass: 'border-sky-200 bg-sky-50 text-sky-900', headerClass: 'border-sky-200 bg-sky-50 text-sky-950', iconClass: 'border-sky-200 bg-sky-100 text-sky-950', sectionClass: 'border-sky-200 bg-[linear-gradient(180deg,rgba(240,249,255,0.96)_0%,rgba(255,255,255,0.99)_24%)]', trackClass: 'border-sky-200 bg-[linear-gradient(180deg,rgba(240,249,255,0.92)_0%,rgba(255,255,255,0.99)_28%)]', }, contract: { label: 'Standard Contracts', shortLabel: 'STD', description: 'General contract templates outside ABO and GDPR, kept in a separate lane for faster scanning.', badgeClass: 'border-slate-300 bg-slate-100 text-slate-800', headerClass: 'border-slate-300 bg-slate-100 text-slate-950', iconClass: 'border-slate-300 bg-white text-slate-950', sectionClass: 'border-slate-300 bg-[linear-gradient(180deg,rgba(248,250,252,0.96)_0%,rgba(255,255,255,0.99)_24%)]', trackClass: 'border-slate-300 bg-[linear-gradient(180deg,rgba(248,250,252,0.94)_0%,rgba(255,255,255,0.99)_28%)]', }, }; const LANGUAGE_ORDER = ['de', 'en', 'other']; function normalizeFamily(type?: string | null): TemplateFamilyKey { if (type === 'contract') return 'contract'; if (type === 'invoice') return 'invoice'; return 'other'; } function normalizeContractType(contractType?: string | null): ContractTypeKey { const value = (contractType || 'contract').toLowerCase(); return ['contract', 'gdpr', 'abo'].includes(value) ? (value as ContractTypeKey) : 'contract'; } function normalizeUserType(userType?: string | null): AudienceKey { const value = (userType || 'both').toLowerCase(); return ['personal', 'company', 'both'].includes(value) ? (value as AudienceKey) : 'both'; } function normalizeLanguage(lang?: string | null) { const value = (lang || '').toLowerCase(); return value === 'de' || value === 'en' ? value : 'other'; } function normalizeName(name?: string | null) { return (name || 'untitled').trim().toLowerCase().replace(/\s+/g, ' '); } function formatUserType(userType?: string | null) { switch (normalizeUserType(userType)) { case 'personal': return 'Privat'; case 'company': return 'Business'; default: return 'Beide'; } } function formatLanguage(lang?: string | null) { switch (normalizeLanguage(lang)) { case 'de': return 'German'; case 'en': return 'English'; default: return 'Other language'; } } function formatLanguageCode(lang?: string | null) { switch (normalizeLanguage(lang)) { case 'de': return 'DE'; case 'en': return 'EN'; default: return 'OT'; } } function formatContractType(contractType?: string | null) { switch (normalizeContractType(contractType)) { case 'abo': return 'ABO Contract'; case 'gdpr': return 'GDPR Contract'; default: return 'Standard Contract'; } } function getContractTypeMeta(contractType?: string | null) { return CONTRACT_TYPE_META[normalizeContractType(contractType)]; } function getTrackContractType(track: TemplateTrack) { return normalizeContractType(track.templates[0]?.contract_type); } function formatTimestamp(value?: string) { if (!value) return null; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return null; return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }).format(parsed); } function compareTemplates(left: ContractTemplate, right: ContractTemplate) { const statusWeight = (status: string) => { if (status === 'published') return 0; if (status === 'draft') return 1; return 2; }; const statusDelta = statusWeight(left.status) - statusWeight(right.status); if (statusDelta !== 0) return statusDelta; const versionDelta = right.version - left.version; if (versionDelta !== 0) return versionDelta; const leftTime = left.updatedAt ? new Date(left.updatedAt).getTime() : 0; const rightTime = right.updatedAt ? new Date(right.updatedAt).getTime() : 0; return rightTime - leftTime; } function buildTrackKey(template: ContractTemplate) { const family = normalizeFamily(template.type); return [ family, normalizeContractType(template.contract_type), normalizeUserType(template.user_type), normalizeName(template.name), ].join(':'); } function buildTrack(templates: ContractTemplate[]): TemplateTrack { const sortedTemplates = [...templates].sort(compareTemplates); const lead = sortedTemplates.find((template) => template.status === 'published') || sortedTemplates[0]; const family = normalizeFamily(lead?.type); const names = Array.from(new Set(sortedTemplates.map((template) => template.name.trim()).filter(Boolean))); const languageKeys = Array.from(new Set(sortedTemplates.map((template) => normalizeLanguage(template.lang)))).sort( (left, right) => LANGUAGE_ORDER.indexOf(left) - LANGUAGE_ORDER.indexOf(right) ); const languageColumns = languageKeys.map((langKey) => { const templatesInLanguage = sortedTemplates.filter((template) => normalizeLanguage(template.lang) === langKey); return { key: langKey, label: formatLanguage(langKey), templates: templatesInLanguage, activeTemplate: templatesInLanguage.find((template) => template.status === 'published'), }; }); const latestUpdatedAt = sortedTemplates.find((template) => template.updatedAt)?.updatedAt; let title = lead?.name || 'Untitled Template'; let subtitle = `${sortedTemplates.length} version${sortedTemplates.length === 1 ? '' : 's'}`; if (family === 'contract') { const contractLabel = formatContractType(lead?.contract_type); title = names[0] || contractLabel; subtitle = `${sortedTemplates.length} version${sortedTemplates.length === 1 ? '' : 's'}`; if (title !== contractLabel) { subtitle = `${contractLabel} • ${subtitle}`; } } if (family === 'invoice') { title = names.length === 1 ? names[0] : 'Invoice Templates'; subtitle = `${formatUserType(lead?.user_type)} • ${sortedTemplates.length} version${sortedTemplates.length === 1 ? '' : 's'}`; } return { key: buildTrackKey(lead), family, title, subtitle, description: lead?.description || undefined, templates: sortedTemplates, languageColumns, activeCount: sortedTemplates.filter((template) => template.status === 'published').length, latestUpdatedAt, }; } function Pill({ children, className }: { children: React.ReactNode; className: string }) { return {children}; } function MetricChip({ label, value }: { label: string; value: React.ReactNode }) { return ( {label} {value} ); } function StatusBadge({ status }: { status: string }) { const map: Record = { draft: 'border-slate-200 bg-slate-100 text-slate-700', published: 'border-emerald-200 bg-emerald-100 text-emerald-800', archived: 'border-amber-200 bg-amber-100 text-amber-800', }; const labels: Record = { draft: 'Draft', published: 'Active', archived: 'Archived', }; const cls = map[status] || 'bg-blue-100 text-blue-800 border border-blue-300'; return {labels[status] || status}; } function TemplateVersionHistoryModal({ history, open, onClose, onEdit, onPreview, onGenPdf, onDownloadPdf, onToggleState, }: { history: VersionHistoryModalState | null; open: boolean; onClose: () => void; onEdit?: (id: string) => void; onPreview: (id: string) => void; onGenPdf: (id: string) => void; onDownloadPdf: (id: string) => void; onToggleState: (id: string, current: string) => Promise; }) { const templates = history?.templates || []; return (
Version history

{history ? `${history.trackTitle} • ${history.languageLabel}` : ''}

Hidden revisions stay out of the main list, but remain available for preview, editing and re-activation here.

{templates.map((template) => (
v{template.version}

{template.name}

{formatTimestamp(template.updatedAt) || 'n/a'}
{onEdit && ( )}
))} {!templates.length && (
No hidden versions for this language.
)}
); } export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) { const { t } = useTranslation(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [q, setQ] = useState(''); const [selectedFamily, setSelectedFamily] = useState('contract'); const [selectedLanguageByTrack, setSelectedLanguageByTrack] = useState>({}); const [pendingToggle, setPendingToggle] = useState<{ id: string; target: 'active' | 'inactive'; message?: string; requiresConfirm: boolean } | null>(null); const [versionHistory, setVersionHistory] = useState(null); const { listTemplates, openPreviewInNewTab, generatePdf, downloadPdf, updateTemplateState, downloadBlobFile, } = useContractManagement(); const filtered = useMemo(() => { const term = q.trim().toLowerCase(); if (!term) return items; return items.filter((item) => { const haystack = [ item.name, item.description || '', item.type || '', item.contract_type || '', item.user_type || '', item.lang || '', item.status, `v${item.version}`, formatContractType(item.contract_type), formatUserType(item.user_type), formatLanguage(item.lang), ] .join(' ') .toLowerCase(); return haystack.includes(term); }); }, [items, q]); const familyGroups = useMemo(() => { const grouped = new Map>(); filtered.forEach((item) => { const family = normalizeFamily(item.type); const trackKey = buildTrackKey(item); if (!grouped.has(family)) { grouped.set(family, new Map()); } const familyMap = grouped.get(family); if (!familyMap) return; if (!familyMap.has(trackKey)) { familyMap.set(trackKey, []); } familyMap.get(trackKey)?.push(item); }); return FAMILY_ORDER.map((family) => { const trackMap = grouped.get(family) || new Map(); const tracks = Array.from(trackMap.values()) .map((templates) => buildTrack(templates)) .sort((left, right) => { const activityDelta = right.activeCount - left.activeCount; if (activityDelta !== 0) return activityDelta; return left.title.localeCompare(right.title); }); const allTemplates = tracks.flatMap((track) => track.templates); return { key: family, label: FAMILY_META[family].label, description: FAMILY_META[family].description, tracks, totalTemplates: allTemplates.length, activeTemplates: allTemplates.filter((template) => template.status === 'published').length, }; }).filter((group) => group.totalTemplates > 0); }, [filtered]); const activeFamily = familyGroups.find((group) => group.key === selectedFamily) || familyGroups[0] || null; const activeContractSections = useMemo(() => { if (!activeFamily || activeFamily.key !== 'contract') return []; return CONTRACT_TYPE_ORDER .map((contractType) => { const tracks = activeFamily.tracks.filter((track) => getTrackContractType(track) === contractType); if (!tracks.length) return null; const templates = tracks.flatMap((track) => track.templates); return { key: contractType, meta: CONTRACT_TYPE_META[contractType], tracks: [...tracks].sort((left, right) => left.title.localeCompare(right.title)), totalTemplates: templates.length, activeTemplates: templates.filter((template) => template.status === 'published').length, }; }) .filter((section): section is ContractTypeSection => Boolean(section)); }, [activeFamily]); const load = async () => { setLoading(true); try { const data = await listTemplates(); const mapped: ContractTemplate[] = data .map((template) => { const normalized = template as NormalizedTemplate; return { id: normalized.id ?? normalized._id ?? normalized.uuid ?? '', name: normalized.name ?? 'Untitled', type: normalized.type, contract_type: normalized.contract_type ?? normalized.contractType ?? null, user_type: normalized.user_type ?? normalized.userType ?? null, lang: normalized.lang ?? null, version: Number(normalized.version ?? 1), status: normalized.state === 'active' ? 'published' : 'draft', updatedAt: normalized.updatedAt ?? normalized.modifiedAt ?? normalized.updated_at, description: normalized.description ?? '', }; }) .filter((template) => template.id); setItems(mapped); } catch { setItems((prev) => prev.length ? prev : [ { id: 'ex1', name: 'Standard Subscription Agreement', type: 'contract', contract_type: 'abo', user_type: 'personal', lang: 'de', version: 2, status: 'published', updatedAt: new Date().toISOString() }, { id: 'ex2', name: 'Standard Subscription Agreement', type: 'contract', contract_type: 'abo', user_type: 'personal', lang: 'de', version: 1, status: 'draft', updatedAt: new Date().toISOString() }, { id: 'ex3', name: 'Invoice Standard', type: 'invoice', user_type: 'both', lang: 'en', version: 3, status: 'published', updatedAt: new Date().toISOString() }, ]); } finally { setLoading(false); } }; useEffect(() => { load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshKey]); useEffect(() => { if (!familyGroups.length) return; if (!familyGroups.some((group) => group.key === selectedFamily)) { setSelectedFamily(familyGroups[0].key); } }, [familyGroups, selectedFamily]); const executeToggleState = async (id: string, target: 'active' | 'inactive') => { try { const updated = await updateTemplateState(id, target as 'active' | 'inactive'); // Update clicked item immediately, then refresh list to reflect any auto-deactivations. setItems((prev) => prev.map((i) => i.id === id ? { ...i, status: updated.state === 'active' ? 'published' : 'draft' } : i)); await load(); } catch {} }; const onToggleState = async (id: string, current: string) => { const target = current === 'published' ? 'inactive' : 'active'; if (target === 'active') { const tpl = items.find((i) => i.id === id); if (tpl) { const kind = tpl.type === 'contract' ? (tpl.contract_type === 'gdpr' ? 'GDPR' : tpl.contract_type === 'abo' ? 'ABO' : 'Contract') : tpl.type === 'invoice' ? 'Invoice' : 'Other'; setPendingToggle({ id, target, requiresConfirm: true, message: `This will deactivate other active ${kind} templates that apply to the same user type and language.`, }); return; } } await executeToggleState(id, target); }; const confirmToggleState = async () => { if (!pendingToggle) return; const shouldCloseVersionHistory = pendingToggle.target === 'active' && Boolean(versionHistory?.templates.some((template) => template.id === pendingToggle.id)); await executeToggleState(pendingToggle.id, pendingToggle.target); if (shouldCloseVersionHistory) { setVersionHistory(null); } setPendingToggle(null); }; const onPreview = (id: string) => openPreviewInNewTab(id); const onGenPdf = async (id: string) => { try { const blob = await generatePdf(id, { preview: true }); downloadBlobFile(blob, `${id}-preview.pdf`); } catch {} }; const onDownloadPdf = async (id: string) => { try { const blob = await downloadPdf(id); downloadBlobFile(blob, `${id}.pdf`); } catch {} }; const renderTrackCard = (track: TemplateTrack) => { const contractMeta = track.family === 'contract' ? getContractTypeMeta(track.templates[0]?.contract_type) : null; const defaultLanguageKey = track.languageColumns.find((languageColumn) => languageColumn.activeTemplate)?.key || track.languageColumns[0]?.key; const selectedLanguageKey = selectedLanguageByTrack[track.key] || defaultLanguageKey; const visibleLanguageColumn = track.languageColumns.find((languageColumn) => languageColumn.key === selectedLanguageKey) || track.languageColumns[0]; if (!visibleLanguageColumn) return null; const primaryTemplate = visibleLanguageColumn.activeTemplate || visibleLanguageColumn.templates[0] || null; const visibleTemplates = primaryTemplate ? [primaryTemplate] : []; const hiddenTemplates = primaryTemplate ? visibleLanguageColumn.templates.filter((template) => template.id !== primaryTemplate.id) : visibleLanguageColumn.templates; const hiddenVersionCount = hiddenTemplates.length; return (
{track.family !== 'contract' && {FAMILY_META[track.family].label}} {track.family === 'contract' && ( {formatUserType(track.templates[0]?.user_type)} )} {track.family === 'invoice' && track.templates[0]?.user_type && ( {formatUserType(track.templates[0].user_type)} )} {track.family === 'other' && track.templates[0]?.user_type && ( {formatUserType(track.templates[0].user_type)} )}
{track.title}

{track.subtitle}

{track.description && (

{track.description}

)}
Languages
{track.languageColumns.map((languageColumn) => { const isSelected = languageColumn.key === selectedLanguageKey; return ( ); })}
{formatLanguageCode(visibleLanguageColumn.key)}
{visibleLanguageColumn.label}
{visibleLanguageColumn.activeTemplate ? `Active version: v${visibleLanguageColumn.activeTemplate.version}` : primaryTemplate ? `Latest version: v${primaryTemplate.version}` : 'No version yet'}
{visibleLanguageColumn.templates.length} version{visibleLanguageColumn.templates.length === 1 ? '' : 's'}
{hiddenVersionCount > 0 && ( )}
{visibleTemplates.map((template) => (
v{template.version}

{template.name}

{formatTimestamp(template.updatedAt) || t('autofix.kee838580')}
{onEdit && ( )}
))}
); }; return (
{t('autofix.k66b39536')}

{t('autofix.k429d94bf')}

{t('autofix.k7e4ef084')}

template.status === 'published').length} />
setQ(e.target.value)} className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-slate-300 focus:ring-4 focus:ring-sky-100" />
{familyGroups.length > 0 && (
{familyGroups.map((familyGroup) => { const familyMeta = FAMILY_META[familyGroup.key]; const isActive = activeFamily?.key === familyGroup.key; return ( ); })}
)} {activeFamily && (
{activeFamily.label}

{activeFamily.label} library

{activeFamily.description}

{activeFamily.key === 'invoice' && (
Invoice templates can target Personal, Company, or Both. Keep one active version per language and user type so the invoice flow stays predictable.
)} {activeFamily.key === 'contract' && (
{t('autofix.k766a5504')}
)} {activeFamily.key === 'contract' ? (
{activeContractSections.map((section) => (
{section.meta.shortLabel}
{t('autofix.kf962066f')}
{section.meta.label}

{section.meta.description}

{section.tracks.length} tracks {section.totalTemplates} versions {section.activeTemplates} active
{section.tracks.map((track) => renderTrackCard(track))}
))}
) : (
{activeFamily.tracks.map((track) => renderTrackCard(track))}
)}
)} {!activeFamily && (
{items.length === 0 ? t('autofix.k3772baff') : t('autofix.k047a175d')}
)}
setPendingToggle(null)} onConfirm={confirmToggleState} /> setVersionHistory(null)} onEdit={onEdit} onPreview={onPreview} onGenPdf={onGenPdf} onDownloadPdf={onDownloadPdf} onToggleState={onToggleState} />
); }