Merge branch 'dev' of https://git.profit-planet.partners/Seazn/profit-planet-frontend into dev
This commit is contained in:
commit
0c8b00d007
@ -29,9 +29,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))
|
||||||
@ -119,46 +122,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) => {
|
||||||
@ -166,12 +205,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')
|
||||||
|
|
||||||
@ -251,9 +304,9 @@ export default function CompanySignContractPage() {
|
|||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
}, 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',
|
||||||
@ -332,13 +385,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>
|
||||||
@ -350,60 +443,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>
|
||||||
@ -560,7 +647,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'}
|
||||||
|
|||||||
@ -33,6 +33,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))
|
||||||
@ -135,19 +136,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.' }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,14 +193,41 @@ export default function PersonalSignContractPage() {
|
|||||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||||
}
|
}
|
||||||
}, [statusLoading, userStatus, smoothReplace])
|
}, [statusLoading, userStatus, smoothReplace])
|
||||||
|
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) => {
|
||||||
@ -207,8 +235,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')
|
||||||
|
|
||||||
@ -278,9 +320,9 @@ export default function PersonalSignContractPage() {
|
|||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
}, 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',
|
||||||
@ -332,13 +374,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>
|
||||||
@ -389,7 +471,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>
|
||||||
@ -489,7 +571,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'}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user