From 2eca3007e470b847f4a9ee682dd7d2fa78a1784f Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Tue, 28 Oct 2025 21:55:47 +0100 Subject: [PATCH] feat: add contract Management --- .../components/contractEditor.tsx | 232 ++++++++++ .../components/contractTemplateList.tsx | 143 ++++++ .../components/contractUploadCompanyStamp.tsx | 435 ++++++++++++++++++ .../hooks/useContractManagement.ts | 435 ++++++++++++++++++ src/app/admin/contract-management/page.tsx | 47 ++ .../delete/deleteConfirmationModal.tsx | 66 +++ 6 files changed, 1358 insertions(+) create mode 100644 src/app/admin/contract-management/components/contractEditor.tsx create mode 100644 src/app/admin/contract-management/components/contractTemplateList.tsx create mode 100644 src/app/admin/contract-management/components/contractUploadCompanyStamp.tsx create mode 100644 src/app/admin/contract-management/hooks/useContractManagement.ts create mode 100644 src/app/admin/contract-management/page.tsx create mode 100644 src/app/components/delete/deleteConfirmationModal.tsx diff --git a/src/app/admin/contract-management/components/contractEditor.tsx b/src/app/admin/contract-management/components/contractEditor.tsx new file mode 100644 index 0000000..21ef233 --- /dev/null +++ b/src/app/admin/contract-management/components/contractEditor.tsx @@ -0,0 +1,232 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; +import useContractManagement from '../hooks/useContractManagement'; + +type Props = { + onSaved?: () => void; +}; + +export default function ContractEditor({ onSaved }: Props) { + const [name, setName] = useState(''); + const [htmlCode, setHtmlCode] = useState(''); + const [isPreview, setIsPreview] = useState(false); + const [saving, setSaving] = useState(false); + const [statusMsg, setStatusMsg] = useState(null); + + const [lang, setLang] = useState<'en' | 'de'>('en'); + const [type, setType] = useState('contract'); + const [description, setDescription] = useState(''); + + const iframeRef = useRef(null); + + const { uploadTemplate, updateTemplateState } = useContractManagement(); + + // Build a full HTML doc if user pasted only a snippet + const wrapIfNeeded = (src: string) => { + const hasDoc = /]/i.test(src); + if (hasDoc) return src; + // Minimal A4 skeleton so snippets render and print correctly + return ` + + + + +Preview + + + +
${src}
+ +`; + }; + + // 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'; + + const save = async (publish: boolean) => { + const html = htmlCode.trim(); + if (!name || !html) { + setStatusMsg('Please enter a template name and content.'); + return; + } + + setSaving(true); + setStatusMsg(null); + + try { + // Build a file from HTML code + const file = new File([html], `${slug(name)}.html`, { type: 'text/html' }); + const created = await uploadTemplate({ + file, + name, + type, + lang, + description: description || undefined, + user_type: 'both', + }); + + if (publish && created?.id) { + await updateTemplateState(created.id, 'active'); + } + + setStatusMsg(publish ? 'Template created and activated.' : 'Template created.'); + if (onSaved) onSaved(); + // Optionally clear fields + // setName(''); setHtmlCode(''); setDescription(''); setType('contract'); setLang('en'); + } catch (e: any) { + setStatusMsg(e?.message || 'Save failed.'); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+ setName(e.target.value)} + className="w-full sm:w-1/2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" + /> +
+ + {isPreview && ( + + )} +
+
+ + {/* New metadata inputs */} +
+ setType(e.target.value)} + className="w-full sm:w-1/3 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" + /> + + setDescription(e.target.value)} + className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" + /> +
+
+ + {!isPreview && ( +