feat: enhance contract management with support for GDPR and contract types in previews
This commit is contained in:
parent
e3198991a9
commit
f144918b6a
@ -16,6 +16,7 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
|
||||
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>('');
|
||||
|
||||
@ -101,6 +102,7 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
htmlCode.trim() &&
|
||||
description.trim() &&
|
||||
type &&
|
||||
(type === 'contract' ? contractType : true) &&
|
||||
userType &&
|
||||
lang
|
||||
)
|
||||
@ -123,6 +125,7 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
file,
|
||||
name,
|
||||
type,
|
||||
contract_type: type === 'contract' ? contractType : undefined,
|
||||
lang,
|
||||
description: description || undefined,
|
||||
user_type: userType,
|
||||
@ -187,6 +190,17 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
<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')}
|
||||
|
||||
@ -10,6 +10,8 @@ type Props = {
|
||||
type ContractTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
contract_type?: string | null;
|
||||
version: number;
|
||||
status: 'draft' | 'published' | 'archived' | string;
|
||||
updatedAt?: string;
|
||||
@ -52,6 +54,8 @@ export default function ContractTemplateList({ refreshKey = 0 }: Props) {
|
||||
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,
|
||||
version: Number(x.version ?? 1),
|
||||
status: (x.state === 'active') ? 'published' : 'draft',
|
||||
updatedAt: x.updatedAt ?? x.modifiedAt ?? x.updated_at,
|
||||
@ -120,6 +124,11 @@ export default function ContractTemplateList({ refreshKey = 0 }: Props) {
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
|
||||
<StatusBadge status={c.status} />
|
||||
{c.type === 'contract' && c.contract_type && (
|
||||
<span className="px-2 py-0.5 rounded text-xs font-semibold bg-indigo-50 text-indigo-800 border border-indigo-200">
|
||||
{c.contract_type === 'gdpr' ? 'GDPR' : 'Contract'}
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
|
||||
@ -5,6 +5,7 @@ export type DocumentTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
contract_type?: 'contract' | 'gdpr' | null | string;
|
||||
lang?: 'en' | 'de' | string;
|
||||
user_type?: 'personal' | 'company' | 'both' | string;
|
||||
state?: 'active' | 'inactive' | string;
|
||||
@ -153,6 +154,7 @@ export default function useContractManagement() {
|
||||
file: File | Blob;
|
||||
name: string;
|
||||
type: string;
|
||||
contract_type?: 'contract' | 'gdpr';
|
||||
lang: 'en' | 'de' | string;
|
||||
description?: string;
|
||||
user_type?: 'personal' | 'company' | 'both';
|
||||
@ -162,6 +164,9 @@ export default function useContractManagement() {
|
||||
fd.append('file', file);
|
||||
fd.append('name', payload.name);
|
||||
fd.append('type', payload.type);
|
||||
if (payload.type === 'contract' && payload.contract_type) {
|
||||
fd.append('contract_type', payload.contract_type);
|
||||
}
|
||||
fd.append('lang', payload.lang);
|
||||
if (payload.description) fd.append('description', payload.description);
|
||||
fd.append('user_type', (payload.user_type ?? 'both'));
|
||||
@ -173,6 +178,7 @@ export default function useContractManagement() {
|
||||
file?: File | Blob;
|
||||
name?: string;
|
||||
type?: string;
|
||||
contract_type?: 'contract' | 'gdpr';
|
||||
lang?: 'en' | 'de' | string;
|
||||
description?: string;
|
||||
user_type?: 'personal' | 'company' | 'both';
|
||||
@ -184,6 +190,9 @@ export default function useContractManagement() {
|
||||
}
|
||||
if (payload.name) fd.append('name', payload.name);
|
||||
if (payload.type) fd.append('type', payload.type);
|
||||
if ((payload.type === 'contract' || payload.contract_type) && payload.contract_type) {
|
||||
fd.append('contract_type', payload.contract_type);
|
||||
}
|
||||
if (payload.lang) fd.append('lang', payload.lang);
|
||||
if (payload.description !== undefined) fd.append('description', payload.description);
|
||||
if (payload.user_type) fd.append('user_type', payload.user_type);
|
||||
|
||||
@ -46,10 +46,13 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
|
||||
const token = useAuthStore(state => state.accessToken)
|
||||
|
||||
// Contract preview state (lazy-loaded)
|
||||
// Contract preview state (lazy-loaded, per contract type)
|
||||
const [previewLoading, setPreviewLoading] = useState(false)
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
|
||||
const [previewState, setPreviewState] = useState({
|
||||
contract: { loading: false, html: null as string | null, error: null as string | null },
|
||||
gdpr: { loading: false, html: null as string | null, error: null as string | null },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && userId && token) {
|
||||
@ -85,6 +88,17 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
}
|
||||
}
|
||||
|
||||
// Load both contract and GDPR previews when modal opens after user is known
|
||||
useEffect(() => {
|
||||
if (!isOpen || !userId || !token || !userDetails) return
|
||||
setPreviewLoading(true)
|
||||
Promise.all([
|
||||
loadContractPreview('contract'),
|
||||
loadContractPreview('gdpr')
|
||||
]).finally(() => setPreviewLoading(false))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, userId, token, userDetails])
|
||||
|
||||
const handleStatusChange = async (newStatus: UserStatus) => {
|
||||
if (!userId || !token || newStatus === selectedStatus) return
|
||||
|
||||
@ -138,19 +152,24 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
}
|
||||
}
|
||||
|
||||
const loadContractPreview = async () => {
|
||||
const loadContractPreview = async (contractType: 'contract' | 'gdpr') => {
|
||||
if (!userId || !token || !userDetails) return
|
||||
setPreviewLoading(true)
|
||||
setPreviewError(null)
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { ...prev[contractType], loading: true, error: null }
|
||||
}))
|
||||
try {
|
||||
const html = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type)
|
||||
setPreviewHtml(html)
|
||||
const html = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type, contractType)
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { loading: false, html, error: null }
|
||||
}))
|
||||
} catch (e: any) {
|
||||
console.error('UserDetailModal.loadContractPreview error:', e)
|
||||
setPreviewError(e?.message || 'Failed to load contract preview')
|
||||
setPreviewHtml(null)
|
||||
} finally {
|
||||
setPreviewLoading(false)
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { loading: false, html: null, error: e?.message || 'Failed to load contract preview' }
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,57 +417,72 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
|
||||
{/* Contract Preview (admin verify flow) */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
|
||||
Contract Preview
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold text-gray-900">Contract Preview</span>
|
||||
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1">
|
||||
{(['contract','gdpr'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setActivePreviewTab(tab)}
|
||||
className={`px-2.5 py-1 text-xs rounded-full transition ${activePreviewTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
|
||||
>
|
||||
{tab === 'contract' ? 'Contract' : 'GDPR'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadContractPreview}
|
||||
disabled={previewLoading}
|
||||
onClick={() => Promise.all([loadContractPreview('contract'), loadContractPreview('gdpr')])}
|
||||
disabled={previewLoading || previewState.contract.loading || previewState.gdpr.loading}
|
||||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
{previewLoading ? 'Loading…' : (previewHtml ? 'Refresh Preview' : 'Load Preview')}
|
||||
{previewLoading || previewState.contract.loading || previewState.gdpr.loading ? 'Loading…' : 'Refresh'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = previewState[activePreviewTab];
|
||||
if (!current?.html) return;
|
||||
const blob = new Blob([current.html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
disabled={!previewState[activePreviewTab]?.html}
|
||||
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
Open in new tab
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!previewHtml) return
|
||||
const blob = new Blob([previewHtml], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}}
|
||||
disabled={!previewHtml}
|
||||
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
Open in new tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-5">
|
||||
{previewError && (
|
||||
{previewState[activePreviewTab].error && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 mb-4">
|
||||
{previewError}
|
||||
{previewState[activePreviewTab].error}
|
||||
</div>
|
||||
)}
|
||||
{previewLoading && (
|
||||
{(previewLoading || previewState[activePreviewTab].loading) && (
|
||||
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
|
||||
Loading preview…
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && previewHtml && (
|
||||
{!previewLoading && !previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
|
||||
<div className="rounded-md border border-gray-200 overflow-hidden">
|
||||
<iframe
|
||||
title="Contract Preview"
|
||||
title={`Contract Preview ${activePreviewTab}`}
|
||||
className="w-full h-[600px] bg-white"
|
||||
srcDoc={previewHtml}
|
||||
srcDoc={previewState[activePreviewTab].html || ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && !previewHtml && !previewError && (
|
||||
<p className="text-sm text-gray-500">Click "Load Preview" to render the latest active contract template for this user.</p>
|
||||
{!previewLoading && !previewState[activePreviewTab].loading && !previewState[activePreviewTab].html && !previewState[activePreviewTab].error && (
|
||||
<p className="text-sm text-gray-500">Click "Refresh" to render the latest active contract or GDPR template for this user.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -598,103 +632,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Section */}
|
||||
{(userDetails.documents.length > 0 || userDetails.contracts.length > 0 || userDetails.idDocuments.length > 0) && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
|
||||
Documents ({userDetails.documents.length + userDetails.contracts.length + userDetails.idDocuments.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
{/* Regular Documents */}
|
||||
{userDetails.documents.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-3">Uploaded Documents</h5>
|
||||
<div className="space-y-2">
|
||||
{userDetails.documents.map((doc) => (
|
||||
<div key={doc.id} className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<DocumentTextIcon className="h-5 w-5 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{doc.file_name}</div>
|
||||
<div className="text-xs text-gray-500">{formatFileSize(doc.file_size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{formatDate(doc.uploaded_at)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contracts */}
|
||||
{userDetails.contracts.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-3">Contracts</h5>
|
||||
<div className="space-y-2">
|
||||
{userDetails.contracts.map((contract) => (
|
||||
<div key={contract.id} className="flex items-center justify-between bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<DocumentTextIcon className="h-5 w-5 text-blue-600" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{contract.file_name}</div>
|
||||
<div className="text-xs text-gray-500">{formatFileSize(contract.file_size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{formatDate(contract.uploaded_at)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ID Documents */}
|
||||
{userDetails.idDocuments.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-3">ID Documents</h5>
|
||||
<div className="space-y-4">
|
||||
{userDetails.idDocuments.map((idDoc) => (
|
||||
<div key={idDoc.id} className="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<IdentificationIcon className="h-5 w-5 text-purple-600" />
|
||||
<span className="text-sm font-medium text-gray-900">{idDoc.document_type}</span>
|
||||
<span className="text-xs text-gray-500 ml-auto">{formatDate(idDoc.uploaded_at)}</span>
|
||||
</div>
|
||||
{(idDoc.frontUrl || idDoc.backUrl) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{idDoc.frontUrl && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-600 mb-2">Front</p>
|
||||
<img
|
||||
src={idDoc.frontUrl}
|
||||
alt="ID Front"
|
||||
className="w-full h-40 object-cover rounded border border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{idDoc.backUrl && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-600 mb-2">Back</p>
|
||||
<img
|
||||
src={idDoc.backUrl}
|
||||
alt="ID Back"
|
||||
className="w-full h-40 object-cover rounded border border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Permissions */}
|
||||
{userDetails.permissions.length > 0 && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
|
||||
@ -192,8 +192,8 @@ export default function Header() {
|
||||
const isLoggedIn = !!user
|
||||
const userPresent = mounted && isLoggedIn
|
||||
|
||||
// NEW: detect admin role across common shapes
|
||||
const isAdmin =
|
||||
// NEW: detect admin role across common shapes (guarded by mount to avoid SSR/CSR mismatch)
|
||||
const rawIsAdmin =
|
||||
!!user &&
|
||||
(
|
||||
(user as any)?.role === 'admin' ||
|
||||
@ -201,6 +201,7 @@ export default function Header() {
|
||||
(user as any)?.isAdmin === true ||
|
||||
((user as any)?.roles?.includes?.('admin'))
|
||||
)
|
||||
const isAdmin = mounted && rawIsAdmin
|
||||
|
||||
return (
|
||||
<header
|
||||
|
||||
@ -25,39 +25,58 @@ export default function PersonalSignContractPage() {
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [previewLoading, setPreviewLoading] = useState(false)
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'contract' | 'gdpr'>('contract')
|
||||
const [previewState, setPreviewState] = useState<{
|
||||
contract: { loading: boolean; html: string | null; error: string | null }
|
||||
gdpr: { loading: boolean; html: string | null; error: string | null }
|
||||
}>({
|
||||
contract: { loading: false, html: null, error: null },
|
||||
gdpr: { loading: false, html: null, error: null }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setDate(new Date().toISOString().slice(0, 10))
|
||||
}, [])
|
||||
|
||||
// Load latest contract preview for personal user
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
if (!accessToken) return
|
||||
setPreviewLoading(true)
|
||||
setPreviewError('')
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include'
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(text || 'Failed to load contract preview')
|
||||
}
|
||||
const html = await res.text()
|
||||
setPreviewHtml(html)
|
||||
} catch (e: any) {
|
||||
console.error('PersonalSignContractPage.loadPreview error:', e)
|
||||
setPreviewError(e?.message || 'Failed to load contract preview')
|
||||
} finally {
|
||||
setPreviewLoading(false)
|
||||
const loadPreview = async (contractType: 'contract' | 'gdpr') => {
|
||||
if (!accessToken) return
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { ...prev[contractType], loading: true, error: null }
|
||||
}))
|
||||
try {
|
||||
const qs = contractType ? `?contract_type=${contractType}` : ''
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include'
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(text || 'Failed to load contract preview')
|
||||
}
|
||||
const html = await res.text()
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { loading: false, html, error: null }
|
||||
}))
|
||||
} catch (e: any) {
|
||||
console.error('PersonalSignContractPage.loadPreview error:', e)
|
||||
setPreviewState((prev) => ({
|
||||
...prev,
|
||||
[contractType]: { loading: false, html: null, error: e?.message || 'Failed to load contract preview' }
|
||||
}))
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
// Load latest contract and GDPR previews for personal user
|
||||
useEffect(() => {
|
||||
if (!accessToken) return
|
||||
setPreviewLoading(true)
|
||||
Promise.all([
|
||||
loadPreview('contract'),
|
||||
loadPreview('gdpr')
|
||||
]).finally(() => setPreviewLoading(false))
|
||||
}, [accessToken])
|
||||
|
||||
const valid = () => {
|
||||
@ -119,13 +138,9 @@ export default function PersonalSignContractPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create FormData for the existing backend endpoint
|
||||
// Create FormData for the backend endpoint (no dummy PDF needed; server generates from templates)
|
||||
const formData = new FormData()
|
||||
formData.append('contractData', JSON.stringify(contractData))
|
||||
// Create a dummy PDF file since the backend expects one (electronic signature)
|
||||
const dummyPdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Electronic Signature) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000079 00000 n \n0000000136 00000 n \n0000000225 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n319\n%%EOF'
|
||||
const dummyFile = new Blob([dummyPdfContent], { type: 'application/pdf' })
|
||||
formData.append('contract', dummyFile, 'electronic_signature.pdf')
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/personal`, {
|
||||
method: 'POST',
|
||||
@ -226,29 +241,44 @@ export default function PersonalSignContractPage() {
|
||||
<div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Contract Preview</h3>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
||||
<span>Contract Preview</span>
|
||||
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1">
|
||||
{(['contract','gdpr'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-2.5 py-1 text-xs rounded-full transition ${activeTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
|
||||
>
|
||||
{tab === 'contract' ? 'Contract' : 'GDPR'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!previewHtml) return
|
||||
const blob = new Blob([previewHtml], { type: 'text/html' })
|
||||
const current = previewState[activeTab];
|
||||
if (!current?.html) return
|
||||
const blob = new Blob([current.html], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}}
|
||||
disabled={!previewHtml}
|
||||
disabled={!previewState[activeTab]?.html}
|
||||
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
|
||||
>
|
||||
Open in new tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{previewLoading ? (
|
||||
{previewLoading || previewState[activeTab].loading ? (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview…</div>
|
||||
) : previewError ? (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
|
||||
) : previewHtml ? (
|
||||
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
|
||||
) : previewState[activeTab].error ? (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewState[activeTab].error}</div>
|
||||
) : previewState[activeTab].html ? (
|
||||
<iframe title={`Contract Preview ${activeTab}`} className="w-full h-72" srcDoc={previewState[activeTab].html || ''} />
|
||||
) : (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
|
||||
)}
|
||||
|
||||
@ -441,12 +441,13 @@ export class AdminAPI {
|
||||
return true
|
||||
}
|
||||
|
||||
static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company') {
|
||||
static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company', contractType?: 'contract' | 'gdpr') {
|
||||
let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_PREVIEW.replace(':id', userId)
|
||||
if (userType) {
|
||||
const qs = new URLSearchParams({ userType }).toString()
|
||||
endpoint += `?${qs}`
|
||||
}
|
||||
const qs = new URLSearchParams()
|
||||
if (userType) qs.set('userType', userType)
|
||||
if (contractType) qs.set('contract_type', contractType)
|
||||
const qsStr = qs.toString()
|
||||
if (qsStr) endpoint += `?${qsStr}`
|
||||
const response = await ApiClient.get(endpoint, token)
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Failed to fetch contract preview' }))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user