feat: enhance contract signing pages with dynamic document previews and error handling

This commit is contained in:
seaznCode 2026-01-14 18:11:29 +01:00
parent 1045debc32
commit 5871416685
2 changed files with 267 additions and 97 deletions

View File

@ -27,9 +27,12 @@ export default function CompanySignContractPage() {
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [success, setSuccess] = useState(false) const [success, setSuccess] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [previewLoading, setPreviewLoading] = useState(false) const [activeTab, setActiveTab] = useState<'contract' | 'gdpr'>('contract')
const [previewHtml, setPreviewHtml] = useState<string | null>(null) const [previewState, setPreviewState] = useState({
const [previewError, setPreviewError] = useState<string | null>(null) 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 }
})
const [previewsReady, setPreviewsReady] = useState(false)
useEffect(() => { useEffect(() => {
setDate(new Date().toISOString().slice(0, 10)) setDate(new Date().toISOString().slice(0, 10))
@ -117,46 +120,82 @@ export default function CompanySignContractPage() {
setSignatureDataUrl('') setSignatureDataUrl('')
} }
// Load latest contract preview for company user const loadPreview = async (contractType: 'contract' | 'gdpr') => {
useEffect(() => {
const loadPreview = async () => {
if (!accessToken) return if (!accessToken) return
setPreviewLoading(true) setPreviewState((prev) => ({
setPreviewError(null) ...prev,
[contractType]: { ...prev[contractType], loading: true, error: null }
}))
try { try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=company`, { const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=${contractType}`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include' credentials: 'include'
}) })
if (!res.ok) { if (!res.ok) {
const text = await res.text().catch(() => '') await res.text().catch(() => null)
throw new Error(text || 'Failed to load contract preview') throw new Error('No contract available at this moment, please contact us.')
} }
const html = await res.text() const html = await res.text()
setPreviewHtml(html) setPreviewState((prev) => ({
} catch (e: any) { ...prev,
[contractType]: { loading: false, html, error: null }
}))
} catch (e: unknown) {
console.error('CompanySignContractPage.loadPreview error:', e) console.error('CompanySignContractPage.loadPreview error:', e)
setPreviewError(e?.message || 'Failed to load contract preview') setPreviewState((prev) => ({
setPreviewHtml(null) ...prev,
} finally { [contractType]: { loading: false, html: null, error: 'No contract available at this moment, please contact us.' }
setPreviewLoading(false) }))
} }
} }
loadPreview()
// Load latest contract + GDPR previews for company user
useEffect(() => {
if (!accessToken) return
loadPreview('contract')
loadPreview('gdpr')
}, [accessToken]) }, [accessToken])
useEffect(() => {
const doneLoading = !previewState.contract.loading && !previewState.gdpr.loading
const anyAvailable = !!previewState.contract.html || !!previewState.gdpr.html
const blockingMsg = 'Temporarily unable to sign contracts. No active documents are available at this moment.'
if (doneLoading) {
setPreviewsReady(true)
// If one preview is missing, default to showing the available one
if (!previewState.contract.html && previewState.gdpr.html) {
setActiveTab('gdpr')
} else if (previewState.contract.html && !previewState.gdpr.html) {
setActiveTab('contract')
}
}
// Only show a blocking error if BOTH are missing; clear it as soon as one exists.
if (doneLoading) {
if (anyAvailable) {
setError((prev) => (prev === blockingMsg ? '' : prev))
} else {
setError(blockingMsg)
}
}
}, [previewState])
const valid = () => { const valid = () => {
const companyValid = companyName.trim().length >= 3 const companyValid = companyName.trim().length >= 3
const repNameValid = repName.trim().length >= 3 const repNameValid = repName.trim().length >= 3
const repTitleValid = repTitle.trim().length >= 2 const repTitleValid = repTitle.trim().length >= 2
const locationValid = location.trim().length >= 2 const locationValid = location.trim().length >= 2
const contractChecked = agreeContract const contractAvailable = !!previewState.contract.html
const dataChecked = agreeData const gdprAvailable = !!previewState.gdpr.html
// Only require acknowledgements for documents that actually exist
const contractChecked = contractAvailable ? agreeContract : true
const dataChecked = gdprAvailable ? agreeData : true
const signatureChecked = confirmSignature const signatureChecked = confirmSignature
const signatureDrawn = !!signatureDataUrl const signatureDrawn = !!signatureDataUrl
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked && signatureDrawn const anyPreview = contractAvailable || gdprAvailable
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked && signatureDrawn && anyPreview
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -164,12 +203,26 @@ export default function CompanySignContractPage() {
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues: string[] = [] const issues: string[] = []
const contractAvailable = !!previewState.contract.html
const gdprAvailable = !!previewState.gdpr.html
if (!contractAvailable && !gdprAvailable) {
const msg = 'Temporarily unable to sign contracts. No active documents are available at this moment.'
setError(msg)
showToast({
variant: 'error',
title: 'No documents available',
message: msg,
})
return
}
if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)') if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)')
if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)') if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)')
if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)') if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)')
if (location.trim().length < 2) issues.push('Location (min 2 characters)') if (location.trim().length < 2) issues.push('Location (min 2 characters)')
if (!agreeContract) issues.push('Contract read and understood') if (contractAvailable && !agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted') if (gdprAvailable && !agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
if (!signatureDataUrl) issues.push('Signature captured on pad') if (!signatureDataUrl) issues.push('Signature captured on pad')
@ -257,9 +310,9 @@ export default function CompanySignContractPage() {
} }
}, 2000) }, 2000)
} catch (error: any) { } catch (error: unknown) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
const msg = error.message || 'Signature failed. Please try again.' const msg = error instanceof Error ? (error.message || 'Signature failed. Please try again.') : 'Signature failed. Please try again.'
setError(msg) setError(msg)
showToast({ showToast({
variant: 'error', variant: 'error',
@ -297,13 +350,53 @@ export default function CompanySignContractPage() {
<section className="grid gap-8 lg:grid-cols-2 mb-10"> <section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50"> <div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-800">Document Information</h2>
<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>
{(() => {
const meta = activeTab === 'contract'
? {
id: 'COMP-2025-001',
title: 'VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG',
version: 'idF 21.05.2025',
jurisdiction: 'EU / Austria (Graz)',
language: 'DE (binding)',
issuer: 'Profit Planet GmbH',
address: 'Liebenauer Hauptstraße 82c, A-8041 Graz'
}
: {
id: 'COMP-GDPR-2025-001',
title: 'SUB-AUFTRAGSVERARBEITUNGS-VERTRAG',
version: 'Art. 28 Abs. 3 DSGVO',
jurisdiction: 'EU / Austria (Graz)',
language: 'DE (binding)',
issuer: 'Profit Planet GmbH',
address: 'Liebenauer Hauptstraße 82c, A-8041 Graz'
}
return (
<ul className="space-y-2 text-xs sm:text-sm text-gray-600"> <ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> COMP-2024-017</li> <li><span className="font-medium text-gray-700">Document:</span> {meta.title}</li>
<li><span className="font-medium text-gray-700">Version:</span> 2.4 (valid from 01.11.2024)</li> <li><span className="font-medium text-gray-700">ID:</span> {meta.id}</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li> <li><span className="font-medium text-gray-700">Version / Basis:</span> {meta.version}</li>
<li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li> <li><span className="font-medium text-gray-700">Jurisdiction:</span> {meta.jurisdiction}</li>
<li><span className="font-medium text-gray-700">Language:</span> {meta.language}</li>
<li><span className="font-medium text-gray-700">Issuer:</span> {meta.issuer}</li>
<li><span className="font-medium text-gray-700">Address:</span> {meta.address}</li>
</ul> </ul>
)
})()}
</div> </div>
<div className="rounded-lg border border-amber-200 bg-amber-50 p-5"> <div className="rounded-lg border border-amber-200 bg-amber-50 p-5">
<h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3> <h3 className="text-sm font-semibold text-amber-900 mb-2">Attention</h3>
@ -315,60 +408,54 @@ export default function CompanySignContractPage() {
<div> <div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden"> <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"> <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">Company Contract Preview</h3> <div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
<span>Document 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"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (!previewHtml) return const current = previewState[activeTab]
const blob = new Blob([previewHtml], { type: 'text/html' }) if (!current?.html) return
const blob = new Blob([current.html], { type: 'text/html' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer') 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" 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 Open in new tab
</button> </button>
<button <button
type="button" type="button"
onClick={async () => { onClick={() => loadPreview(activeTab)}
if (!accessToken) return disabled={previewState[activeTab].loading}
setPreviewLoading(true)
setPreviewError(null)
try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=company`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60" className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
> >
{previewLoading ? 'Loading…' : 'Refresh'} {previewState[activeTab].loading ? 'Loading…' : 'Refresh'}
</button> </button>
</div> </div>
</div> </div>
{previewLoading ? ( {previewState[activeTab].loading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div> <div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewError ? ( ) : previewState[activeTab].error ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div> <div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewState[activeTab].error}</div>
) : previewHtml ? ( ) : previewState[activeTab].html ? (
<iframe title="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} /> <iframe title={`Company Document 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> <div className="h-72 flex items-center justify-center text-xs text-gray-500">No contract available at this moment, please contact us.</div>
)} )}
</div> </div>
</div> </div>
@ -525,7 +612,7 @@ export default function CompanySignContractPage() {
</button> </button>
<button <button
type="submit" type="submit"
disabled={submitting || success} disabled={submitting || success || (!previewState.contract.html && !previewState.gdpr.html)}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition" className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
> >
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'} {submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}

View File

@ -31,6 +31,7 @@ export default function PersonalSignContractPage() {
contract: { loading: false, html: null, error: null }, contract: { loading: false, html: null, error: null },
gdpr: { loading: false, html: null, error: null } gdpr: { loading: false, html: null, error: null }
}) })
const [previewsReady, setPreviewsReady] = useState(false)
useEffect(() => { useEffect(() => {
setDate(new Date().toISOString().slice(0, 10)) setDate(new Date().toISOString().slice(0, 10))
@ -133,19 +134,19 @@ export default function PersonalSignContractPage() {
credentials: 'include' credentials: 'include'
}) })
if (!res.ok) { if (!res.ok) {
const text = await res.text().catch(() => '') await res.text().catch(() => null)
throw new Error(text || 'Failed to load contract preview') throw new Error('No contract available at this moment, please contact us.')
} }
const html = await res.text() const html = await res.text()
setPreviewState((prev) => ({ setPreviewState((prev) => ({
...prev, ...prev,
[contractType]: { loading: false, html, error: null } [contractType]: { loading: false, html, error: null }
})) }))
} catch (e: any) { } catch (e: unknown) {
console.error('PersonalSignContractPage.loadPreview error:', e) console.error('PersonalSignContractPage.loadPreview error:', e)
setPreviewState((prev) => ({ setPreviewState((prev) => ({
...prev, ...prev,
[contractType]: { loading: false, html: null, error: e?.message || 'Failed to load contract preview' } [contractType]: { loading: false, html: null, error: 'No contract available at this moment, please contact us.' }
})) }))
} }
} }
@ -160,13 +161,41 @@ export default function PersonalSignContractPage() {
]).finally(() => setPreviewLoading(false)) ]).finally(() => setPreviewLoading(false))
}, [accessToken]) }, [accessToken])
useEffect(() => {
const doneLoading = !previewState.contract.loading && !previewState.gdpr.loading && !previewLoading
const anyAvailable = !!previewState.contract.html || !!previewState.gdpr.html
const blockingMsg = 'Temporarily unable to sign contracts. No active documents are available at this moment.'
if (doneLoading) {
setPreviewsReady(true)
}
if (doneLoading) {
if (anyAvailable) {
setError((prev) => (prev === blockingMsg ? '' : prev))
} else {
setError(blockingMsg)
}
// If one preview is missing, default to showing the available one
if (!previewState.contract.html && previewState.gdpr.html) {
setActiveTab('gdpr')
} else if (previewState.contract.html && !previewState.gdpr.html) {
setActiveTab('contract')
}
}
}, [previewState, previewLoading])
const valid = () => { const valid = () => {
const contractChecked = agreeContract const contractAvailable = !!previewState.contract.html
const dataChecked = agreeData const gdprAvailable = !!previewState.gdpr.html
// Only require acknowledgements for documents that actually exist
const contractChecked = contractAvailable ? agreeContract : true
const dataChecked = gdprAvailable ? agreeData : true
const signatureChecked = confirmSignature const signatureChecked = confirmSignature
const signatureDrawn = !!signatureDataUrl const signatureDrawn = !!signatureDataUrl
const anyPreview = contractAvailable || gdprAvailable
return contractChecked && dataChecked && signatureChecked && signatureDrawn return contractChecked && dataChecked && signatureChecked && signatureDrawn && anyPreview
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -174,8 +203,22 @@ export default function PersonalSignContractPage() {
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues: string[] = [] const issues: string[] = []
if (!agreeContract) issues.push('Contract read and understood') const contractAvailable = !!previewState.contract.html
if (!agreeData) issues.push('Privacy policy accepted') const gdprAvailable = !!previewState.gdpr.html
if (!contractAvailable && !gdprAvailable) {
const msg = 'Temporarily unable to sign contracts. No active documents are available at this moment.'
setError(msg)
showToast({
variant: 'error',
title: 'No documents available',
message: msg,
})
return
}
if (contractAvailable && !agreeContract) issues.push('Contract read and understood')
if (gdprAvailable && !agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
if (!signatureDataUrl) issues.push('Signature captured on pad') if (!signatureDataUrl) issues.push('Signature captured on pad')
@ -253,9 +296,9 @@ export default function PersonalSignContractPage() {
} }
}, 2000) }, 2000)
} catch (error: any) { } catch (error: unknown) {
console.error('Contract signing error:', error) console.error('Contract signing error:', error)
const msg = error.message || 'Signature failed. Please try again.' const msg = error instanceof Error ? (error.message || 'Signature failed. Please try again.') : 'Signature failed. Please try again.'
setError(msg) setError(msg)
showToast({ showToast({
variant: 'error', variant: 'error',
@ -297,13 +340,53 @@ export default function PersonalSignContractPage() {
<section className="grid gap-8 lg:grid-cols-2 mb-10"> <section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50"> <div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-800">Document Information</h2>
<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>
{(() => {
const meta = activeTab === 'contract'
? {
id: 'PERS-2025-001',
title: 'VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG',
version: 'idF 21.05.2025',
jurisdiction: 'EU / Austria (Graz)',
language: 'DE (binding)',
issuer: 'Profit Planet GmbH',
address: 'Liebenauer Hauptstraße 82c, A-8041 Graz'
}
: {
id: 'PERS-GDPR-2025-001',
title: 'SUB-AUFTRAGSVERARBEITUNGS-VERTRAG',
version: 'Art. 28 Abs. 3 DSGVO',
jurisdiction: 'EU / Austria (Graz)',
language: 'DE (binding)',
issuer: 'Profit Planet GmbH',
address: 'Liebenauer Hauptstraße 82c, A-8041 Graz'
}
return (
<ul className="space-y-2 text-xs sm:text-sm text-gray-600"> <ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li> <li><span className="font-medium text-gray-700">Document:</span> {meta.title}</li>
<li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li> <li><span className="font-medium text-gray-700">ID:</span> {meta.id}</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li> <li><span className="font-medium text-gray-700">Version / Basis:</span> {meta.version}</li>
<li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li> <li><span className="font-medium text-gray-700">Jurisdiction:</span> {meta.jurisdiction}</li>
<li><span className="font-medium text-gray-700">Language:</span> {meta.language}</li>
<li><span className="font-medium text-gray-700">Issuer:</span> {meta.issuer}</li>
<li><span className="font-medium text-gray-700">Address:</span> {meta.address}</li>
</ul> </ul>
)
})()}
</div> </div>
<div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5"> <div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5">
<h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3> <h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3>
@ -354,7 +437,7 @@ export default function PersonalSignContractPage() {
) : previewState[activeTab].html ? ( ) : previewState[activeTab].html ? (
<iframe title={`Contract Preview ${activeTab}`} className="w-full h-72" srcDoc={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> <div className="h-72 flex items-center justify-center text-xs text-gray-500">No contract available at this moment, please contact us.</div>
)} )}
</div> </div>
</div> </div>
@ -454,7 +537,7 @@ export default function PersonalSignContractPage() {
</button> </button>
<button <button
type="submit" type="submit"
disabled={submitting || success} disabled={submitting || success || (!previewState.contract.html && !previewState.gdpr.html)}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition" className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
> >
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'} {submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}