222 lines
9.1 KiB
TypeScript
222 lines
9.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import useContractManagement from '../hooks/useContractManagement';
|
|
import ConfirmActionModal from '../../../components/modals/ConfirmActionModal';
|
|
|
|
type Props = {
|
|
refreshKey?: number;
|
|
onEdit?: (id: string) => void;
|
|
};
|
|
|
|
type ContractTemplate = {
|
|
id: string;
|
|
name: string;
|
|
type?: string;
|
|
contract_type?: string | null;
|
|
user_type?: string | null;
|
|
version: number;
|
|
status: 'draft' | 'published' | 'archived' | string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
function Pill({ children, className }: { children: React.ReactNode; className: string }) {
|
|
return <span className={`px-2 py-0.5 rounded text-xs font-semibold border ${className}`}>{children}</span>;
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: string }) {
|
|
const map: Record<string, string> = {
|
|
draft: 'bg-gray-100 text-gray-800 border border-gray-300',
|
|
published: 'bg-green-100 text-green-800 border border-green-300',
|
|
archived: 'bg-yellow-100 text-yellow-800 border border-yellow-300',
|
|
};
|
|
const cls = map[status] || 'bg-blue-100 text-blue-800 border border-blue-300';
|
|
return <span className={`px-2 py-0.5 rounded text-xs font-semibold ${cls}`}>{status}</span>;
|
|
}
|
|
|
|
export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props) {
|
|
const [items, setItems] = useState<ContractTemplate[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [q, setQ] = useState('');
|
|
const [pendingToggle, setPendingToggle] = useState<{ id: string; target: 'active' | 'inactive'; message?: string; requiresConfirm: boolean } | null>(null)
|
|
|
|
const {
|
|
listTemplates,
|
|
openPreviewInNewTab,
|
|
generatePdf,
|
|
downloadPdf,
|
|
updateTemplateState,
|
|
downloadBlobFile,
|
|
} = useContractManagement();
|
|
|
|
const filtered = useMemo(() => {
|
|
const term = q.trim().toLowerCase();
|
|
if (!term) return items;
|
|
return items.filter((i) => i.name.toLowerCase().includes(term) || String(i.version).includes(term) || i.status.toLowerCase().includes(term));
|
|
}, [items, q]);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await listTemplates();
|
|
const mapped: ContractTemplate[] = data.map((x: any) => ({
|
|
id: x.id ?? x._id ?? x.uuid,
|
|
name: x.name ?? 'Untitled',
|
|
type: x.type,
|
|
contract_type: x.contract_type ?? x.contractType ?? null,
|
|
user_type: x.user_type ?? x.userType ?? null,
|
|
version: Number(x.version ?? 1),
|
|
status: (x.state === 'active') ? 'published' : 'draft',
|
|
updatedAt: x.updatedAt ?? x.modifiedAt ?? x.updated_at,
|
|
}));
|
|
setItems(mapped);
|
|
} catch {
|
|
setItems((prev) => prev.length ? prev : [
|
|
{ id: 'ex1', name: 'Sample Contract A', version: 1, status: 'draft', updatedAt: new Date().toISOString() },
|
|
{ id: 'ex2', name: 'NDA Template', version: 3, status: 'published', updatedAt: new Date().toISOString() },
|
|
]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [refreshKey]);
|
|
|
|
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
|
|
await executeToggleState(pendingToggle.id, pendingToggle.target)
|
|
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 {}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900">
|
|
Invoice templates always use user type "Both". Provide templates for each language (en/de). If no active invoice template matches, backend falls back to a text-only invoice.
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<input
|
|
placeholder="Search templates…"
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
className="w-full rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50 shadow"
|
|
/>
|
|
<button
|
|
onClick={load}
|
|
disabled={loading}
|
|
className="rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-4 py-2 text-sm font-medium shadow disabled:opacity-60"
|
|
>
|
|
{loading ? 'Loading…' : 'Refresh'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{filtered.map((c) => (
|
|
<div key={c.id} className="rounded-xl border border-gray-100 bg-white shadow-sm p-4 flex flex-col gap-2 hover:shadow-md transition">
|
|
<p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<StatusBadge status={c.status} />
|
|
{c.type && (
|
|
<Pill className="bg-slate-50 text-slate-800 border-slate-200">
|
|
{c.type === 'contract' ? 'Contract' : c.type === 'invoice' ? 'Invoice' : 'Other'}
|
|
</Pill>
|
|
)}
|
|
{c.type === 'contract' && (
|
|
<Pill className="bg-indigo-50 text-indigo-800 border-indigo-200">
|
|
{c.contract_type === 'gdpr' ? 'GDPR' : c.contract_type === 'abo' ? 'ABO' : 'Contract'}
|
|
</Pill>
|
|
)}
|
|
{c.user_type && c.type !== 'invoice' && (
|
|
<Pill className="bg-emerald-50 text-emerald-800 border-emerald-200">
|
|
{c.user_type === 'personal' ? 'Personal' : c.user_type === 'company' ? 'Company' : 'Both'}
|
|
</Pill>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{onEdit && (
|
|
<button
|
|
onClick={() => onEdit(c.id)}
|
|
className="px-3 py-1 text-xs rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 transition"
|
|
>
|
|
Edit
|
|
</button>
|
|
)}
|
|
<button onClick={() => onPreview(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">Preview</button>
|
|
<button onClick={() => onGenPdf(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">PDF</button>
|
|
<button onClick={() => onDownloadPdf(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">Download</button>
|
|
<button onClick={() => onToggleState(c.id, c.status)} className={`px-3 py-1 text-xs rounded-lg font-semibold transition
|
|
${c.status === 'published'
|
|
? 'bg-red-100 hover:bg-red-200 text-red-700 border border-red-200'
|
|
: 'bg-indigo-600 hover:bg-indigo-500 text-white border border-indigo-600'}`}>
|
|
{c.status === 'published' ? 'Deactivate' : 'Activate'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!filtered.length && (
|
|
<div className="col-span-full py-8 text-center text-sm text-gray-500">No contracts found.</div>
|
|
)}
|
|
</div>
|
|
|
|
<ConfirmActionModal
|
|
open={Boolean(pendingToggle?.requiresConfirm)}
|
|
title="Activate template now?"
|
|
description={pendingToggle?.message || 'This action will update template activation status.'}
|
|
confirmText="Activate"
|
|
onClose={() => setPendingToggle(null)}
|
|
onConfirm={confirmToggleState}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|