diff --git a/frontend/src/hooks/useContracts.ts b/frontend/src/hooks/useContracts.ts index 99f9254..cb571ad 100644 --- a/frontend/src/hooks/useContracts.ts +++ b/frontend/src/hooks/useContracts.ts @@ -205,7 +205,7 @@ export const useContracts = (filters?: { template_version: c.template_version, scope: c.scope as ContractScope, status: c.status, - content: c.content, + content: c.content_html || c.content, // Backend returns content_html customer: c.customer ? String(c.customer) : undefined, customer_name: c.customer_name || undefined, customer_email: c.customer_email || undefined, @@ -219,7 +219,7 @@ export const useContracts = (filters?: { expires_at: c.expires_at, voided_at: c.voided_at, voided_reason: c.voided_reason, - public_token: c.public_token, + public_token: c.signing_token || c.public_token, // Backend returns signing_token created_at: c.created_at, updated_at: c.updated_at, })); @@ -258,7 +258,7 @@ export const useContract = (id: string) => { expires_at: data.expires_at, voided_at: data.voided_at, voided_reason: data.voided_reason, - public_token: data.public_token, + public_token: data.signing_token || data.public_token, // Backend returns signing_token created_at: data.created_at, updated_at: data.updated_at, }; @@ -270,9 +270,9 @@ export const useContract = (id: string) => { interface ContractInput { template: string; - customer?: string; - appointment?: string; - service?: string; + customer_id?: string; + event_id?: string; + send_email?: boolean; } /** @@ -352,8 +352,8 @@ export const usePublicContract = (token: string) => { return useQuery({ queryKey: ['public-contracts', token], queryFn: async () => { - // Use a plain axios instance without auth - const { data } = await apiClient.get(`/contracts/public/${token}/`); + // Use the public signing endpoint + const { data } = await apiClient.get(`/contracts/sign/${token}/`); return data; }, enabled: !!token, @@ -368,21 +368,57 @@ export const useSignContract = () => { return useMutation({ mutationFn: async ({ token, - signature_data, signer_name, - signer_email, + consent_checkbox_checked, + electronic_consent_given, }: { token: string; - signature_data: string; signer_name: string; - signer_email: string; + consent_checkbox_checked: boolean; + electronic_consent_given: boolean; }) => { - const { data } = await apiClient.post(`/contracts/public/${token}/sign/`, { - signature_data, + const { data } = await apiClient.post(`/contracts/sign/${token}/`, { signer_name, - signer_email, + consent_checkbox_checked, + electronic_consent_given, }); return data; }, }); }; + +/** + * Hook to export a legal compliance package for a signed contract + */ +export const useExportLegalPackage = () => { + return useMutation({ + mutationFn: async (id: string) => { + const response = await apiClient.get(`/contracts/${id}/export_legal/`, { + responseType: 'blob', + }); + + // Create download link + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + + // Extract filename from Content-Disposition header if available + const contentDisposition = response.headers['content-disposition']; + let filename = `legal_export_${id}.zip`; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="?([^";\n]+)"?/); + if (filenameMatch && filenameMatch[1]) { + filename = filenameMatch[1]; + } + } + + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + return { success: true }; + }, + }); +}; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 1a03af1..d6224cd 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -70,7 +70,28 @@ "rememberMe": "Remember me", "twoFactorRequired": "Two-factor authentication required", "enterCode": "Enter verification code", - "verifyCode": "Verify Code" + "verifyCode": "Verify Code", + "login": { + "title": "Sign in to your account", + "subtitle": "Don't have an account?", + "createAccount": "Create one now", + "platformBadge": "Platform Login", + "heroTitle": "Manage Your Business with Confidence", + "heroSubtitle": "Access your dashboard to manage appointments, customers, and grow your business.", + "features": { + "scheduling": "Smart scheduling & resource management", + "automation": "Automated reminders & follow-ups", + "security": "Enterprise-grade security" + }, + "privacy": "Privacy", + "terms": "Terms" + }, + "tenantLogin": { + "welcome": "Welcome to {{business}}", + "subtitle": "Sign in to manage your appointments", + "staffAccess": "Staff Access", + "customerBooking": "Customer Booking" + } }, "nav": { "dashboard": "Dashboard", @@ -679,6 +700,7 @@ "edit": "Edit", "viewDetails": "View Details", "copyLink": "Copy Signing Link", + "copied": "Copied!", "sendEmail": "Send Email", "openSigningPage": "Open Signing Page", "saveChanges": "Save Changes" @@ -723,7 +745,11 @@ "status": "Status", "created": "Created", "contentPreview": "Content Preview", - "signingLink": "Signing Link" + "signingLink": "Signing Link", + "signedTitle": "Contract Successfully Signed", + "viewFullContract": "View Full Contract", + "printContract": "Print Contract", + "exportLegal": "Export Legal Package" }, "preview": { "title": "Preview Contract", @@ -861,7 +887,14 @@ "noCustomersFound": "No customers found matching your search.", "addNewCustomer": "Add New Customer", "createCustomer": "Create Customer", - "errorLoading": "Error loading customers" + "errorLoading": "Error loading customers", + "deleteCustomer": "Delete Customer", + "deleteConfirmation": "Are you sure you want to delete this customer? This action cannot be undone.", + "password": "Password", + "newPassword": "New Password", + "passwordPlaceholder": "Leave blank to keep current password", + "accountInfo": "Account Information", + "contactDetails": "Contact Details" }, "resources": { "title": "Resources", diff --git a/frontend/src/pages/ContractSigning.tsx b/frontend/src/pages/ContractSigning.tsx index dbfb2bd..266d64e 100644 --- a/frontend/src/pages/ContractSigning.tsx +++ b/frontend/src/pages/ContractSigning.tsx @@ -1,36 +1,43 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { FileSignature, CheckCircle, XCircle, Loader } from 'lucide-react'; +import { FileSignature, CheckCircle, XCircle, Loader, Printer, Download } from 'lucide-react'; import { usePublicContract, useSignContract } from '../hooks/useContracts'; +import { useQueryClient } from '@tanstack/react-query'; const ContractSigning: React.FC = () => { const { t } = useTranslation(); const { token } = useParams<{ token: string }>(); + const queryClient = useQueryClient(); const [signerName, setSignerName] = useState(''); - const [signerEmail, setSignerEmail] = useState(''); const [agreedToTerms, setAgreedToTerms] = useState(false); - const [signatureData, setSignatureData] = useState(''); + const [electronicConsent, setElectronicConsent] = useState(false); - const { data: contractData, isLoading, error } = usePublicContract(token || ''); + const { data: contractData, isLoading, error, refetch } = usePublicContract(token || ''); const signMutation = useSignContract(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!token || !signatureData || !agreedToTerms) return; + if (!token || !signerName.trim() || !agreedToTerms || !electronicConsent) return; try { await signMutation.mutateAsync({ token, - signature_data: signatureData, - signer_name: signerName, - signer_email: signerEmail, + signer_name: signerName.trim(), + consent_checkbox_checked: agreedToTerms, + electronic_consent_given: electronicConsent, }); + // Refetch to get updated contract with signature data + await refetch(); } catch (error) { console.error('Failed to sign contract:', error); } }; + const handlePrint = () => { + window.print(); + }; + if (isLoading) { return (
@@ -42,7 +49,8 @@ const ContractSigning: React.FC = () => { ); } - if (error || !contractData) { + if (error || !contractData || !contractData.contract) { + console.error('Contract loading error:', { error, contractData }); return (
@@ -58,27 +66,6 @@ const ContractSigning: React.FC = () => { ); } - if (contractData.contract.status === 'SIGNED') { - return ( -
-
- -

- {t('contracts.signing.alreadySigned')} -

- {contractData.signature && ( -

- {t('contracts.signing.signedBy', { - name: contractData.signature.signer_name, - date: new Date(contractData.signature.signed_at).toLocaleDateString(), - })} -

- )} -
-
- ); - } - if (contractData.is_expired) { return (
@@ -95,22 +82,155 @@ const ContractSigning: React.FC = () => { ); } - if (signMutation.isSuccess) { + // Show signed contract view + if (contractData.contract?.status === 'SIGNED' || signMutation.isSuccess) { return ( -
-
- -

- {t('contracts.signing.success')} -

-

- {t('contracts.signing.thankYou')} -

+
+
+ {/* Print Header - Only visible when printing */} +
+
+
+

{contractData.business.name}

+

{contractData.template.name}

+
+
+

Contract ID: {contractData.contract.id}

+

Status: SIGNED

+
+
+
+ + {/* Success Banner - Hidden when printing */} +
+
+ +
+

+ Contract Successfully Signed +

+

+ This contract has been signed and is now legally binding. +

+
+
+
+ + {/* Action Buttons - Hidden when printing */} +
+ +
+ + {/* Contract Header */} +
+
+ {contractData.business.logo_url ? ( + {contractData.business.name} + ) : ( + + )} +
+

+ {contractData.business.name} +

+

{contractData.template.name}

+
+
+ {contractData.customer && ( +

+ Contract for: {contractData.customer.name} + {contractData.customer.email && ` (${contractData.customer.email})`} +

+ )} +
+ + {/* Contract Content */} +
+
+
+ + {/* Signature Details */} +
+

+ Electronic Signature Record +

+ +
+
+
+

Signer Name

+

+ {contractData.signature?.signer_name || signerName || 'N/A'} +

+
+
+

Signer Email

+

+ {contractData.signature?.signer_email || contractData.customer?.email || 'N/A'} +

+
+
+

Date Signed

+

+ {contractData.signature?.signed_at + ? new Date(contractData.signature.signed_at).toLocaleString() + : new Date().toLocaleString() + } +

+
+
+ +
+
+

Electronic Signature

+
+

+ {contractData.signature?.signer_name || signerName || 'N/A'} +

+
+
+
+

Contract Status

+ + + Signed + +
+
+
+ + {/* Legal Notice */} +
+

+ This document was signed electronically in compliance with the Electronic Signatures in Global and National Commerce Act (ESIGN Act, 15 U.S.C. section 7001 et seq.) and the Uniform Electronic Transactions Act (UETA). The signer expressly agreed to the terms and conditions, consented to conduct business electronically, and acknowledged that their electronic signature is the legal equivalent of a handwritten signature. A complete audit trail including timestamp, IP address, and document hash is maintained. +

+
+
+ + {/* Print Footer - Only visible when printing */} +
+

Printed on: {new Date().toLocaleString()}

+

This is a copy of an electronically signed document.

+
); } + // Show signing form return (
@@ -143,7 +263,7 @@ const ContractSigning: React.FC = () => {
{/* Contract Content */} -
+
{

-
+
+ {/* Electronic Signature - Type Name */}
-
- -
- - setSignerEmail(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
- -
- -
-