420 lines
20 KiB
TypeScript
420 lines
20 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import PageLayout from '../../../components/PageLayout'
|
|
import useAuthStore from '../../../store/authStore'
|
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
|
import { API_BASE_URL } from '../../../utils/api'
|
|
|
|
export default function CompanySignContractPage() {
|
|
const router = useRouter()
|
|
const { accessToken } = useAuthStore()
|
|
const { refreshStatus } = useUserStatus()
|
|
|
|
const [companyName, setCompanyName] = useState('')
|
|
const [repName, setRepName] = useState('')
|
|
const [repTitle, setRepTitle] = useState('')
|
|
const [location, setLocation] = useState('')
|
|
const [date, setDate] = useState('')
|
|
const [note, setNote] = useState('')
|
|
const [agreeContract, setAgreeContract] = useState(false)
|
|
const [agreeData, setAgreeData] = useState(false)
|
|
const [confirmSignature, setConfirmSignature] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
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)
|
|
|
|
useEffect(() => {
|
|
setDate(new Date().toISOString().slice(0,10))
|
|
}, [])
|
|
|
|
// Load latest contract preview for company user
|
|
useEffect(() => {
|
|
const loadPreview = async () => {
|
|
if (!accessToken) return
|
|
setPreviewLoading(true)
|
|
setPreviewError(null)
|
|
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('CompanySignContractPage.loadPreview error:', e)
|
|
setPreviewError(e?.message || 'Failed to load contract preview')
|
|
setPreviewHtml(null)
|
|
} finally {
|
|
setPreviewLoading(false)
|
|
}
|
|
}
|
|
loadPreview()
|
|
}, [accessToken])
|
|
|
|
const valid = () => {
|
|
const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name
|
|
const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name
|
|
const repTitleValid = repTitle.trim().length >= 2 // Min 2 characters for title
|
|
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
|
const contractChecked = agreeContract
|
|
const dataChecked = agreeData
|
|
const signatureChecked = confirmSignature
|
|
|
|
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!valid()) {
|
|
// Detailed error message to help debug
|
|
const issues = []
|
|
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 (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)')
|
|
if (location.trim().length < 2) issues.push('Location (min 2 characters)')
|
|
if (!agreeContract) issues.push('Contract read and understood')
|
|
if (!agreeData) issues.push('Privacy policy accepted')
|
|
if (!confirmSignature) issues.push('Electronic signature confirmed')
|
|
|
|
setError(`Please complete: ${issues.join(', ')}`)
|
|
return
|
|
}
|
|
|
|
if (!accessToken) {
|
|
setError('Not authenticated. Please log in again.')
|
|
return
|
|
}
|
|
|
|
setError('')
|
|
setSubmitting(true)
|
|
|
|
try {
|
|
const contractData = {
|
|
companyName: companyName.trim(),
|
|
representativeName: repName.trim(),
|
|
representativeTitle: repTitle.trim(),
|
|
location: location.trim(),
|
|
date,
|
|
note: note.trim() || null,
|
|
contractType: 'company',
|
|
confirmations: {
|
|
agreeContract,
|
|
agreeData,
|
|
confirmSignature
|
|
}
|
|
}
|
|
|
|
// Create FormData for the existing backend endpoint
|
|
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/company`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
// Don't set Content-Type, let browser set it for FormData
|
|
},
|
|
body: formData
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: 'Contract signing failed' }))
|
|
throw new Error(errorData.message || 'Contract signing failed')
|
|
}
|
|
|
|
setSuccess(true)
|
|
|
|
// Refresh user status to update contract signed state
|
|
await refreshStatus()
|
|
|
|
// Redirect to main dashboard after short delay
|
|
setTimeout(() => {
|
|
// Check if we came from tutorial
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const fromTutorial = urlParams.get('tutorial') === 'true'
|
|
|
|
if (fromTutorial) {
|
|
router.push('/quickaction-dashboard?tutorial=true')
|
|
} else {
|
|
router.push('/quickaction-dashboard')
|
|
}
|
|
}, 2000)
|
|
|
|
} catch (error: any) {
|
|
console.error('Contract signing error:', error)
|
|
setError(error.message || 'Signature failed. Please try again.')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div className="relative flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
|
|
{/* Background */}
|
|
<div className="fixed inset-0 -z-10">
|
|
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
|
<svg aria-hidden="true" className="absolute inset-0 -z-10 h-full w-full stroke-white/10">
|
|
<defs>
|
|
<pattern id="company-contract-pattern" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse">
|
|
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
|
|
</pattern>
|
|
</defs>
|
|
<rect fill="url(#company-contract-pattern)" width="100%" height="100%" strokeWidth={0} />
|
|
</svg>
|
|
<div aria-hidden="true" className="absolute top-0 right-0 left-1/2 -ml-24 blur-3xl transform-gpu overflow-hidden lg:ml-24 xl:ml-48">
|
|
<div
|
|
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
|
|
style={{ clipPath:'polygon(63.1% 29.5%,100% 17.1%,76.6% 3%,48.4% 0%,44.6% 4.7%,54.5% 25.3%,59.8% 49%,55.2% 57.8%,44.4% 57.2%,27.8% 47.9%,35.1% 81.5%,0% 97.7%,39.2% 100%,35.2% 81.4%,97.2% 52.8%,63.1% 29.5%)'}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
|
>
|
|
<div className="px-6 py-8 sm:px-10 lg:px-14">
|
|
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
|
|
Sign Company Partnership Contract
|
|
</h1>
|
|
<p className="text-center text-sm text-gray-600 mb-8">
|
|
Please review the contract details and sign on behalf of the company.
|
|
</p>
|
|
|
|
{/* Meta + Preview */}
|
|
<section className="grid gap-8 lg:grid-cols-2 mb-10">
|
|
<div className="space-y-4">
|
|
<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>
|
|
<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">Version:</span> 2.4 (valid from 01.11.2024)</li>
|
|
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
|
|
<li><span className="font-medium text-gray-700">Language:</span> DE (binding)</li>
|
|
</ul>
|
|
</div>
|
|
<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>
|
|
<p className="text-xs sm:text-sm text-amber-800 leading-relaxed">
|
|
You confirm that you are authorized to sign on behalf of the company.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<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">Company Contract Preview</h3>
|
|
<div className="flex items-center gap-2">
|
|
<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 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>
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (!accessToken) return
|
|
setPreviewLoading(true)
|
|
setPreviewError(null)
|
|
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 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"
|
|
>
|
|
{previewLoading ? 'Loading…' : 'Refresh'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{previewLoading ? (
|
|
<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="Company Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
|
|
) : (
|
|
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<hr className="my-10 border-gray-200" />
|
|
|
|
{/* Company Signature Fields */}
|
|
<section>
|
|
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
|
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
<div className="sm:col-span-2 lg:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Company Name *
|
|
</label>
|
|
<input
|
|
value={companyName}
|
|
onChange={e => { setCompanyName(e.target.value); setError('') }}
|
|
placeholder="Firmenname offiziell"
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Date *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={date}
|
|
onChange={e => setDate(e.target.value)}
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text_sm font-medium text-gray-700 mb-1">
|
|
Location *
|
|
</label>
|
|
<input
|
|
value={location}
|
|
onChange={e => { setLocation(e.target.value); setError('') }}
|
|
placeholder="z.B. München"
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="sm:col-span-2 lg:col-span-1">
|
|
<label className="block text_sm font-medium text-gray-700 mb-1">
|
|
Representative Name *
|
|
</label>
|
|
<input
|
|
value={repName}
|
|
onChange={e => { setRepName(e.target.value); setError('') }}
|
|
placeholder="Vor- und Nachname"
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="sm:col-span-2 lg:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Representative Position / Title *
|
|
</label>
|
|
<input
|
|
value={repTitle}
|
|
onChange={e => { setRepTitle(e.target.value); setError('') }}
|
|
placeholder="z.B. Geschäftsführer, Authorized Signatory"
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="sm:col-span-2 lg:col-span-3">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Note (optional)
|
|
</label>
|
|
<input
|
|
value={note}
|
|
onChange={e => setNote(e.target.value)}
|
|
placeholder="Interne Referenz / Zusatz"
|
|
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<hr className="my-10 border-gray-200" />
|
|
|
|
{/* Confirmations */}
|
|
<section className="space-y-5">
|
|
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
|
|
<label className="flex items-start gap-3 text-sm text-gray-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={agreeContract}
|
|
onChange={e => setAgreeContract(e.target.checked)}
|
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
/>
|
|
<span>I confirm I have read and accepted the full contract on behalf of the company.</span>
|
|
</label>
|
|
<label className="flex items-start gap-3 text-sm text-gray-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={agreeData}
|
|
onChange={e => setAgreeData(e.target.checked)}
|
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
/>
|
|
<span>I consent to processing of company and personal data in accordance with the privacy policy.</span>
|
|
</label>
|
|
<label className="flex items-start gap-3 text-sm text-gray-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={confirmSignature}
|
|
onChange={e => setConfirmSignature(e.target.checked)}
|
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
/>
|
|
<span>I am authorized to sign legally binding documents for this company.</span>
|
|
</label>
|
|
</section>
|
|
|
|
{error && (
|
|
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{success && (
|
|
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
|
|
Contract signed successfully. Redirecting shortly…
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-10 flex justify-end">
|
|
<button
|
|
type="submit"
|
|
disabled={submitting || success}
|
|
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'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|