profit-planet-frontend/src/app/admin/contract-management/components/contractEditor.tsx

368 lines
13 KiB
TypeScript

'use client';
import React, { useEffect, useRef, useState } from 'react';
import useContractManagement from '../hooks/useContractManagement';
type Props = {
editingTemplateId?: string | null;
onCancelEdit?: () => void;
onSaved?: (info?: { action: 'created' | 'revised'; templateId: string }) => void;
};
export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdit }: Props) {
const [name, setName] = useState('');
const [htmlCode, setHtmlCode] = useState('');
const [isPreview, setIsPreview] = useState(false);
const [saving, setSaving] = useState(false);
const [statusMsg, setStatusMsg] = useState<string | null>(null);
const [lang, setLang] = useState<'en' | 'de'>('en');
const [type, setType] = useState<'contract' | 'bill' | 'other'>('contract');
const [contractType, setContractType] = useState<'contract' | 'gdpr'>('contract');
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
const [description, setDescription] = useState<string>('');
const [editingMeta, setEditingMeta] = useState<{ id: string; version: number; state: string } | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const { uploadTemplate, updateTemplateState, getTemplate, reviseTemplate } = useContractManagement();
const resetEditorFields = () => {
setName('');
setHtmlCode('');
setDescription('');
setIsPreview(false);
setEditingMeta(null);
};
// Load template into editor when editing
useEffect(() => {
const load = async () => {
if (!editingTemplateId) {
setEditingMeta(null);
return;
}
setSaving(true);
setStatusMsg(null);
try {
const tpl = await getTemplate(editingTemplateId);
setName(tpl.name || '');
setHtmlCode(tpl.html || '');
setDescription((tpl.description as any) || '');
setLang((tpl.lang as any) || 'en');
setType((tpl.type as any) || 'contract');
setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr');
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
setEditingMeta({
id: editingTemplateId,
version: Number(tpl.version || 1),
state: String(tpl.state || 'inactive')
});
setStatusMsg(`Loaded template for editing (v${Number(tpl.version || 1)}).`);
} catch (e: any) {
setStatusMsg(e?.message || 'Failed to load template.');
} finally {
setSaving(false);
}
};
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingTemplateId]);
// Build a full HTML doc if user pasted only a snippet
const wrapIfNeeded = (src: string) => {
const hasDoc = /<!DOCTYPE|<html[\s>]/i.test(src);
if (hasDoc) return src;
// Minimal A4 skeleton so snippets render and print correctly
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Preview</title>
<style>
@page { size: A4; margin: 0; }
html, body { margin:0; padding:0; background:#eee; }
body { display:flex; justify-content:center; }
.a4 { width:210mm; min-height:297mm; background:#fff; box-shadow:0 0 5mm rgba(0,0,0,0.1); box-sizing:border-box; padding:20mm; }
@media print {
html, body { background:#fff; }
.a4 { box-shadow:none; margin:0; }
}
</style>
</head>
<body>
<div class="a4">${src}</div>
</body>
</html>`;
};
// Write/refresh iframe preview
useEffect(() => {
if (!isPreview) return;
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc) return;
const html = wrapIfNeeded(htmlCode);
doc.open();
doc.write(html);
doc.close();
const resize = () => {
// Allow time for layout/styles
requestAnimationFrame(() => {
const h = doc.body ? Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight) : 1200;
iframe.style.height = Math.min(Math.max(h, 1123), 2000) + 'px'; // clamp for UX
});
};
// Initial resize and after load
resize();
const onLoad = () => resize();
iframe.addEventListener('load', onLoad);
// Also observe mutations to adjust height if content changes
const mo = new MutationObserver(resize);
mo.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });
return () => {
iframe.removeEventListener('load', onLoad);
mo.disconnect();
};
}, [isPreview, htmlCode]);
const printPreview = () => {
const w = iframeRef.current?.contentWindow;
w?.focus();
w?.print();
};
const slug = (s: string) =>
s.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'template';
// NEW: all-fields-required guard
const canSave = Boolean(
name.trim() &&
htmlCode.trim() &&
type &&
(type === 'contract' ? contractType : true) &&
userType &&
lang
)
const save = async (publish: boolean) => {
const html = htmlCode.trim();
// NEW: validate all fields
if (!canSave) {
setStatusMsg('Please fill all required fields (name, HTML, type, user type, language).');
return;
}
if (publish && type === 'contract') {
const kind = contractType === 'gdpr' ? 'GDPR' : 'Contract';
const ok = window.confirm(
`Activate this ${kind} template now?\n\nThis will deactivate other active ${kind} templates that apply to the same user type and language.`
);
if (!ok) return;
}
setSaving(true);
setStatusMsg(null);
try {
// Build a file from HTML code
const file = new File([html], `${slug(name)}.html`, { type: 'text/html' });
// If editing: revise (new object + version bump) and deactivate previous
if (editingTemplateId) {
const revised = await reviseTemplate(editingTemplateId, {
file,
name,
type,
contract_type: type === 'contract' ? contractType : undefined,
lang,
description: description || undefined,
user_type: userType,
state: publish ? 'active' : 'inactive',
});
setStatusMsg(publish ? 'New version created and activated (previous deactivated).' : 'New version created (previous deactivated).');
if (onSaved && revised?.id) onSaved({ action: 'revised', templateId: String(revised.id) });
resetEditorFields();
return;
}
// Otherwise: create new
const created = await uploadTemplate({
file,
name,
type,
contract_type: type === 'contract' ? contractType : undefined,
lang,
description: description || undefined,
user_type: userType,
});
if (publish && created?.id) {
await updateTemplateState(created.id, 'active');
}
setStatusMsg(publish ? 'Template created and activated.' : 'Template created.');
if (onSaved && created?.id) onSaved({ action: 'created', templateId: String(created.id) });
// Reset so another template can be created immediately
resetEditorFields();
} catch (e: any) {
setStatusMsg(e?.message || 'Save failed.');
} finally {
setSaving(false);
}
};
return (
<div className="space-y-6">
{editingMeta && (
<div className="rounded-lg border border-indigo-200 bg-indigo-50 px-4 py-3 flex items-center justify-between gap-3">
<div className="text-sm text-indigo-900">
<span className="font-semibold">Editing:</span> {name || 'Untitled'} (v{editingMeta.version}) state: {editingMeta.state}
</div>
{onCancelEdit && (
<button
type="button"
onClick={() => {
resetEditorFields();
onCancelEdit();
}}
className="inline-flex items-center rounded-lg bg-white hover:bg-gray-50 text-gray-900 px-3 py-1.5 text-sm font-medium shadow border border-gray-200 transition"
>
Cancel editing
</button>
)}
</div>
)}
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row gap-4">
<input
type="text"
placeholder="Template name"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="w-full sm:w-1/2 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"
/>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setIsPreview((v) => !v)}
className="inline-flex items-center rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-900 px-4 py-2 text-sm font-medium shadow transition"
>
{isPreview ? 'Switch to Code' : 'Preview HTML'}
</button>
{isPreview && (
<button
type="button"
onClick={printPreview}
className="inline-flex items-center rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-4 py-2 text-sm font-medium shadow transition"
>
Print
</button>
)}
</div>
</div>
{/* New metadata inputs */}
<div className="flex flex-col sm:flex-row gap-4">
<select
value={type}
onChange={(e) => setType(e.target.value as 'contract' | 'bill' | 'other')}
required
className="w-full sm:w-1/3 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"
>
<option value="contract">Contract</option>
<option value="bill">Bill</option>
<option value="other">Other</option>
</select>
{type === 'contract' && (
<select
value={contractType}
onChange={(e) => setContractType(e.target.value as 'contract' | 'gdpr')}
required
className="w-full sm:w-40 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"
>
<option value="contract">Contract</option>
<option value="gdpr">GDPR</option>
</select>
)}
<select
value={userType}
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
required
className="w-full sm:w-40 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"
>
<option value="personal">Personal</option>
<option value="company">Company</option>
<option value="both">Both</option>
</select>
<select
value={lang}
onChange={(e) => setLang(e.target.value as 'en' | 'de')}
required
className="w-full sm:w-32 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"
>
<option value="en">English (en)</option>
<option value="de">Deutsch (de)</option>
</select>
<input
type="text"
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(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"
/>
</div>
</div>
{!isPreview && (
<textarea
value={htmlCode}
onChange={(e) => setHtmlCode(e.target.value)}
placeholder="Paste your full HTML (or snippet) here…"
required
className="min-h-[320px] w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 font-mono shadow"
/>
)}
{isPreview && (
<div className="rounded-lg border border-gray-300 bg-white shadow">
<iframe
ref={iframeRef}
title="Contract Preview"
className="w-full rounded-lg"
style={{ height: 1200, background: 'transparent' }}
/>
</div>
)}
<div className="flex items-center gap-4">
<button
onClick={() => save(false)}
disabled={saving || !canSave}
className="inline-flex items-center rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-900 px-4 py-2 text-sm font-medium shadow disabled:opacity-60 transition"
>
Create (inactive)
</button>
<button
onClick={() => save(true)}
disabled={saving || !canSave}
className="inline-flex items-center rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow disabled:opacity-60 transition"
>
Create & Activate
</button>
{/* NEW: helper text */}
{!canSave && <span className="text-xs text-red-600">Fill all fields to proceed.</span>}
{saving && <span className="text-xs text-gray-500">Saving</span>}
{statusMsg && <span className="text-xs text-gray-600">{statusMsg}</span>}
</div>
</div>
);
}