599 lines
28 KiB
TypeScript
599 lines
28 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useRef, useCallback } 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'
|
|
import { useToast } from '../../../components/toast/toastComponent'
|
|
import { useTranslation } from '../../../i18n/useTranslation'
|
|
|
|
export default function CompanySignContractPage() {
|
|
const { t } = useTranslation();
|
|
const router = useRouter()
|
|
const user = useAuthStore(s => s.user) // NEW
|
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
|
const { accessToken } = useAuthStore()
|
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
|
const { showToast } = useToast()
|
|
|
|
const [date, setDate] = useState('')
|
|
const [signatureDataUrl, setSignatureDataUrl] = 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 [activeTab, setActiveTab] = 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(() => {
|
|
setDate(new Date().toISOString().slice(0, 10))
|
|
}, [])
|
|
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
const isDrawing = useRef(false)
|
|
|
|
const getPos = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return { x: 0, y: 0 }
|
|
const rect = canvas.getBoundingClientRect()
|
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
|
|
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
|
|
// Coordinates should be in CSS pixels. The canvas context is already DPR-scaled via setTransform.
|
|
const x = clientX - rect.left
|
|
const y = clientY - rect.top
|
|
return { x, y }
|
|
}
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const resize = () => {
|
|
const rect = canvas.getBoundingClientRect()
|
|
const dpr = window.devicePixelRatio || 1
|
|
canvas.width = rect.width * dpr
|
|
canvas.height = rect.height * dpr
|
|
const ctx = canvas.getContext('2d')
|
|
if (ctx) {
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
ctx.lineWidth = 2
|
|
ctx.lineCap = 'round'
|
|
ctx.strokeStyle = '#0f172a'
|
|
}
|
|
}
|
|
resize()
|
|
window.addEventListener('resize', resize)
|
|
return () => window.removeEventListener('resize', resize)
|
|
}, [])
|
|
|
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault()
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
ctx.lineWidth = 2
|
|
ctx.lineCap = 'round'
|
|
ctx.strokeStyle = '#0f172a'
|
|
const { x, y } = getPos(e)
|
|
ctx.beginPath()
|
|
ctx.moveTo(x, y)
|
|
isDrawing.current = true
|
|
}
|
|
|
|
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault()
|
|
if (!isDrawing.current) return
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
const { x, y } = getPos(e)
|
|
ctx.lineTo(x, y)
|
|
ctx.stroke()
|
|
}
|
|
|
|
const endDrawing = () => {
|
|
if (!isDrawing.current) return
|
|
isDrawing.current = false
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const dataUrl = canvas.toDataURL('image/png')
|
|
setSignatureDataUrl(dataUrl)
|
|
}
|
|
|
|
const clearSignature = () => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
setSignatureDataUrl('')
|
|
}
|
|
|
|
const loadPreview = async (contractType: 'contract' | 'gdpr') => {
|
|
if (!accessToken) return
|
|
setPreviewState((prev) => ({
|
|
...prev,
|
|
[contractType]: { ...prev[contractType], loading: true, error: null }
|
|
}))
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=${contractType}`, {
|
|
method: 'GET',
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
credentials: 'include'
|
|
})
|
|
if (!res.ok) {
|
|
await res.text().catch(() => null)
|
|
throw new Error('No contract available at this moment, please contact us.')
|
|
}
|
|
const html = await res.text()
|
|
setPreviewState((prev) => ({
|
|
...prev,
|
|
[contractType]: { loading: false, html, error: null }
|
|
}))
|
|
} catch (e: unknown) {
|
|
console.error('CompanySignContractPage.loadPreview error:', e)
|
|
setPreviewState((prev) => ({
|
|
...prev,
|
|
[contractType]: { loading: false, html: null, error: 'No contract available at this moment, please contact us.' }
|
|
}))
|
|
}
|
|
}
|
|
|
|
// Load latest contract + GDPR previews for company user
|
|
useEffect(() => {
|
|
if (!accessToken) return
|
|
setPreviewLoading(true)
|
|
Promise.all([
|
|
loadPreview('contract'),
|
|
loadPreview('gdpr')
|
|
]).finally(() => setPreviewLoading(false))
|
|
}, [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) {
|
|
// 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, previewLoading])
|
|
|
|
const valid = () => {
|
|
const contractAvailable = !!previewState.contract.html
|
|
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 signatureDrawn = !!signatureDataUrl
|
|
|
|
const anyPreview = contractAvailable || gdprAvailable
|
|
return contractChecked && dataChecked && signatureChecked && signatureDrawn && anyPreview
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!valid()) {
|
|
// Detailed error message to help debug
|
|
const issues: string[] = []
|
|
const contractAvailable = !!previewState.contract.html
|
|
const gdprAvailable = !!previewState.gdpr.html
|
|
|
|
if (!contractAvailable && !gdprAvailable) {
|
|
const msg = t('quickactionDashboard.contractSigning.noDocumentsAvailableMessage')
|
|
setError(msg)
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('quickactionDashboard.contractSigning.noDocumentsAvailableTitle'),
|
|
message: msg,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (contractAvailable && !agreeContract) issues.push(t('quickactionDashboard.contractSigning.contractReadUnderstood'))
|
|
if (gdprAvailable && !agreeData) issues.push(t('quickactionDashboard.contractSigning.privacyAccepted'))
|
|
if (!confirmSignature) issues.push(t('quickactionDashboard.contractSigning.electronicSignatureConfirmed'))
|
|
if (!signatureDataUrl) issues.push(t('quickactionDashboard.contractSigning.signatureCaptured'))
|
|
|
|
const msg = `${t('quickactionDashboard.contractSigning.completePrefix')} ${issues.join(', ')}`
|
|
setError(msg)
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('quickactionDashboard.contractSigning.missingInformationTitle'),
|
|
message: msg,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (!accessToken) {
|
|
const msg = t('quickactionDashboard.contractSigning.authErrorMessage')
|
|
setError(msg)
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('quickactionDashboard.contractSigning.authErrorTitle'),
|
|
message: msg,
|
|
})
|
|
return
|
|
}
|
|
|
|
setError('')
|
|
setSubmitting(true)
|
|
|
|
try {
|
|
const contractData = {
|
|
date,
|
|
contractType: 'company',
|
|
}
|
|
|
|
// Create FormData for the existing backend endpoint
|
|
const formData = new FormData()
|
|
formData.append('contractData', JSON.stringify(contractData))
|
|
if (signatureDataUrl) {
|
|
formData.append('signatureImage', signatureDataUrl)
|
|
}
|
|
|
|
const response = await fetch(`${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: t('autofix.keac0c9e7') }))
|
|
throw new Error(errorData.message || 'Contract signing failed')
|
|
}
|
|
|
|
setSuccess(true)
|
|
showToast({
|
|
variant: 'success',
|
|
title: t('quickactionDashboard.contractSigning.contractSignedTitle'),
|
|
message: t('quickactionDashboard.contractSigning.companyContractSignedMessage'),
|
|
})
|
|
|
|
// Refresh user status to update contract signed state
|
|
await refreshStatus()
|
|
|
|
// Redirect back to tutorial modal (avoid intermediate dashboard redirect)
|
|
suppressAutoRedirectRef.current = true
|
|
smoothReplace('/quickaction-dashboard?tutorial=true')
|
|
|
|
} catch (error: unknown) {
|
|
console.error('Contract signing error:', error)
|
|
const msg = error instanceof Error ? (error.message || t('quickactionDashboard.contractSigning.signingFailedMessage')) : t('quickactionDashboard.contractSigning.signingFailedMessage')
|
|
setError(msg)
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('quickactionDashboard.contractSigning.signingFailedTitle'),
|
|
message: msg,
|
|
})
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
// NEW: hard block if step already done OR all steps done
|
|
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
|
const redirectOnceRef = useRef(false)
|
|
const suppressAutoRedirectRef = useRef(false)
|
|
const smoothReplace = useCallback((to: string) => {
|
|
if (redirectOnceRef.current) return
|
|
redirectOnceRef.current = true
|
|
setRedirectTo(to)
|
|
window.setTimeout(() => router.replace(to), 200)
|
|
}, [router])
|
|
|
|
useEffect(() => {
|
|
if (statusLoading || !userStatus) return
|
|
if (suppressAutoRedirectRef.current) return
|
|
const allDone =
|
|
!!userStatus.email_verified &&
|
|
!!userStatus.documents_uploaded &&
|
|
!!userStatus.profile_completed &&
|
|
!!userStatus.contract_signed
|
|
|
|
if (allDone) {
|
|
smoothReplace('/dashboard') // CHANGED
|
|
} else if (userStatus.contract_signed) {
|
|
smoothReplace('/quickaction-dashboard') // CHANGED
|
|
}
|
|
}, [statusLoading, userStatus, smoothReplace])
|
|
|
|
// NEW: must be logged in
|
|
useEffect(() => {
|
|
if (!isAuthReady) return
|
|
if (!user || !accessToken) smoothReplace('/login')
|
|
}, [isAuthReady, user, accessToken, smoothReplace])
|
|
|
|
return (
|
|
<PageLayout>
|
|
{/* NEW: smooth redirect overlay */}
|
|
{redirectTo && (
|
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
|
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
|
<div className="text-sm font-medium text-gray-900">{t('quickactionDashboard.redirecting')}</div>
|
|
<div className="mt-1 text-xs text-gray-600">{t('quickactionDashboard.pleaseWait')}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
|
<div className="pointer-events-none absolute inset-0 z-0">
|
|
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
|
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
|
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
|
</div>
|
|
|
|
{/* CHANGED: tighter padding on mobile */}
|
|
<main className="relative z-10 flex flex-col flex-1 w-full px-4 sm:px-8 lg:px-12 py-6 sm:py-10 lg:py-12">
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
|
|
>
|
|
{/* CHANGED: tighter padding on mobile */}
|
|
<div className="px-4 py-6 sm:px-10 sm:py-8 lg:px-14">
|
|
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
|
|
{t('quickactionDashboard.contractSigning.companyTitle')}
|
|
</h1>
|
|
<p className="text-center text-sm text-gray-600 mb-8">
|
|
{t('quickactionDashboard.contractSigning.companySubtitle')}
|
|
</p>
|
|
|
|
{/* CHANGED: smaller gaps on mobile */}
|
|
<section className="grid gap-6 sm:gap-8 lg:grid-cols-2 mb-8 sm:mb-10">
|
|
<div className="space-y-4">
|
|
{/* CHANGED: tighter padding */}
|
|
<div className="rounded-lg border border-gray-200 p-4 sm:p-5 bg-gray-50">
|
|
{/* CHANGED: stack header + tabs on mobile */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-3">
|
|
<h2 className="text-sm font-semibold text-gray-800">{t('quickactionDashboard.contractSigning.documentInformation')}</h2>
|
|
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1 w-fit max-w-full overflow-x-auto">
|
|
{(['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 whitespace-nowrap ${activeTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
|
|
>
|
|
{tab === 'contract' ? t('quickactionDashboard.contractSigning.contractTab') : t('quickactionDashboard.contractSigning.gdprTab')}
|
|
</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">
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.documentLabel')}</span> {meta.title}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.idLabel')}</span> {meta.id}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.versionLabel')}</span> {meta.version}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.jurisdictionLabel')}</span> {meta.jurisdiction}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.languageLabel')}</span> {meta.language}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.issuerLabel')}</span> {meta.issuer}</li>
|
|
<li><span className="font-medium text-gray-700">{t('quickactionDashboard.contractSigning.addressLabel')}</span> {meta.address}</li>
|
|
</ul>
|
|
)
|
|
})()}
|
|
</div>
|
|
{/* CHANGED: tighter padding */}
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 sm:p-5">
|
|
<h3 className="text-sm font-semibold text-amber-900 mb-2">{t('quickactionDashboard.contractSigning.attentionTitle')}</h3>
|
|
<p className="text-xs sm:text-sm text-amber-800 leading-relaxed">
|
|
{t('quickactionDashboard.contractSigning.attentionBody')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
|
|
{/* CHANGED: stack toolbar on mobile */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 border-b border-gray-200 bg-gray-50">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 text-sm font-semibold text-gray-900">
|
|
<span>{t('quickactionDashboard.contractSigning.documentPreview')}</span>
|
|
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1 w-fit max-w-full overflow-x-auto">
|
|
{(['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 whitespace-nowrap ${activeTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
|
|
>
|
|
{tab === 'contract' ? t('quickactionDashboard.contractSigning.contractTab') : t('quickactionDashboard.contractSigning.gdprTab')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* CHANGED: buttons wrap + full width on mobile */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
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={!previewState[activeTab]?.html}
|
|
className="w-full sm:w-auto inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
|
|
>
|
|
{t('quickactionDashboard.contractSigning.openInNewTab')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => loadPreview(activeTab)}
|
|
disabled={previewState[activeTab].loading}
|
|
className="w-full sm:w-auto inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
|
|
>
|
|
{previewState[activeTab].loading ? t('quickactionDashboard.contractSigning.loadingPreview') : t('quickactionDashboard.contractSigning.refresh')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CHANGED: shorter on mobile */}
|
|
{previewLoading || previewState[activeTab].loading ? (
|
|
<div className="h-64 sm:h-72 flex items-center justify-center text-xs text-gray-500">{t('quickactionDashboard.contractSigning.loadingPreview')}</div>
|
|
) : previewState[activeTab].error ? (
|
|
<div className="h-64 sm: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={`Company Document Preview ${activeTab}`} className="w-full h-64 sm:h-72" srcDoc={previewState[activeTab].html || ''} />
|
|
) : (
|
|
<div className="h-64 sm:h-72 flex items-center justify-center text-xs text-gray-500">{t('quickactionDashboard.contractSigning.noContractAvailable')}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<hr className="my-10 border-gray-200" />
|
|
|
|
<section>
|
|
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">{t('quickactionDashboard.contractSigning.signatureSection')}</h2>
|
|
|
|
<div className="">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t('quickactionDashboard.contractSigning.drawSignature')}
|
|
</label>
|
|
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={800}
|
|
height={220}
|
|
className="w-full h-40 sm:h-48 rounded-md border border-gray-200 bg-white shadow-inner touch-none"
|
|
onMouseDown={startDrawing}
|
|
onMouseMove={draw}
|
|
onMouseUp={endDrawing}
|
|
onMouseLeave={endDrawing}
|
|
onTouchStart={startDrawing}
|
|
onTouchMove={draw}
|
|
onTouchEnd={endDrawing}
|
|
/>
|
|
{/* CHANGED: stack helper row on mobile */}
|
|
<div className="mt-3 flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 text-xs text-gray-600">
|
|
<button
|
|
type="button"
|
|
onClick={clearSignature}
|
|
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50"
|
|
>
|
|
{t('quickactionDashboard.contractSigning.clear')}
|
|
</button>
|
|
<span className="text-gray-500">{t('quickactionDashboard.contractSigning.signatureHelp')}</span>
|
|
{signatureDataUrl && <span className="text-green-600 font-medium">{t('quickactionDashboard.contractSigning.captured')}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<hr className="my-10 border-gray-200" />
|
|
|
|
<section className="space-y-5">
|
|
<h2 className="text-sm font-semibold text-[#0F2460]">{t('quickactionDashboard.contractSigning.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>{t('quickactionDashboard.contractSigning.confirmContractCompany')}</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>{t('quickactionDashboard.contractSigning.confirmDataCompany')}</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>{t('quickactionDashboard.contractSigning.confirmSignatureCompany')}</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">
|
|
{t('quickactionDashboard.contractSigning.contractSignedRedirecting')}
|
|
</div>
|
|
)}
|
|
|
|
{/* CHANGED: stack bottom actions on mobile */}
|
|
<div className="mt-10 flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => router.push('/quickaction-dashboard')}
|
|
className="w-full sm:w-auto inline-flex items-center justify-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
|
|
>
|
|
{t('quickactionDashboard.backToDashboard')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={submitting || success || (!previewState.contract.html && !previewState.gdpr.html)}
|
|
className="w-full sm:w-auto 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 ? t('quickactionDashboard.contractSigning.signing') : success ? t('quickactionDashboard.contractSigning.signed') : t('quickactionDashboard.contractSigning.signNow')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</main>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|