import { useCallback } from 'react'; import useAuthStore from '../../../store/authStore'; export type DocumentTemplate = { id: string; name: string; type?: string; contract_type?: 'contract' | 'gdpr' | 'abo' | null | string; lang?: 'en' | 'de' | string; user_type?: 'personal' | 'company' | 'both' | string; tax_mode?: 'standard' | 'reverse_charge' | 'both' | null | string; state?: 'active' | 'inactive' | string; version?: number; previewUrl?: string | null; fileUrl?: string | null; html?: string | null; updatedAt?: string; }; export type CompanyStamp = { id: string; label?: string | null; mimeType?: string; base64?: string | null; // normalized base64 or data URI active?: boolean; createdAt?: string; // ...other metadata... }; type Json = Record; function redactLongOrBase64ish(value: any) { if (typeof value !== 'string') return value; const len = value.length; const head = value.slice(0, 32); // If it's long, treat it as potentially sensitive / base64 and only log metadata. if (len > 200) { return { kind: 'string', len, head }; } return value; } function redactJsonForLogs(input: any): any { if (!input || typeof input !== 'object') return redactLongOrBase64ish(input); if (Array.isArray(input)) return input.map(redactJsonForLogs); const out: Record = {}; for (const [k, v] of Object.entries(input)) { if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) { out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) }; } else { out[k] = redactJsonForLogs(v); } } return out; } function isFormData(body: any): body is FormData { return typeof FormData !== 'undefined' && body instanceof FormData; } function safeDescribeBody(body: any) { if (!body) return null; if (isFormData(body)) { const entries: Record = {}; try { for (const [k, v] of body.entries()) { if (typeof File !== 'undefined' && v instanceof File) { entries[k] = { kind: 'File', name: v.name, type: v.type, size: v.size }; } else { // Strings only for our current usage. entries[k] = v; } } } catch (e: any) { return { kind: 'FormData', error: e?.message || String(e) }; } return { kind: 'FormData', entries }; } if (typeof body === 'string') { return { kind: 'string', preview: body.slice(0, 500), length: body.length }; } // Avoid dumping arbitrary objects (could be huge / sensitive) return { kind: typeof body }; } export default function useContractManagement() { const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''; const getState = useAuthStore.getState; const authorizedFetch = useCallback( async ( path: string, init: RequestInit = {}, responseType: 'json' | 'text' | 'blob' = 'json' ): Promise => { let token = getState().accessToken; if (!token) { const ok = await getState().refreshAuthToken(); if (ok) token = getState().accessToken; } const headers: Record = { ...(init.headers as Record || {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), }; // Do not set Content-Type for FormData; browser will set proper boundary if (!isFormData(init.body) && init.method && init.method !== 'GET') { headers['Content-Type'] = headers['Content-Type'] || 'application/json'; } const url = `${base}${path}`; const method = init.method || 'GET'; // Debug (safe) try { console.debug('[CM] fetch ->', { url, method, hasAuth: !!token, }); } catch {} // EXTRA debug for document-template calls: show what we send (safe metadata only) if (path.startsWith('/api/document-templates')) { try { const safeHeaders = { ...headers } as Record; if (safeHeaders.Authorization) safeHeaders.Authorization = '[redacted]'; console.info('[CM][document-templates] request', { url, method, headers: safeHeaders, body: safeDescribeBody(init.body), }); } catch {} } // Include cookies + Authorization on all requests const res = await fetch(`${base}${path}`, { credentials: 'include', ...init, headers, }); try { console.debug('[CM] fetch <-', { path, status: res.status, ok: res.ok, ct: res.headers.get('content-type') }); } catch {} if (!res.ok) { const text = await res.text().catch(() => ''); console.warn('[CM] fetch error body:', text?.slice(0, 2000)); throw new Error(`HTTP ${res.status}: ${text || res.statusText}`); } // Log and return body by responseType if (responseType === 'blob') { const len = res.headers.get('content-length'); try { console.debug('[CM] fetch body (blob):', { contentType: res.headers.get('content-type'), contentLength: len ? Number(len) : null }); } catch {} return (await res.blob()) as unknown as T; } if (responseType === 'text') { const text = await res.text(); try { console.debug('[CM] fetch body (text):', text.slice(0, 2000)); } catch {} return text as unknown as T; } // json (default): read text once, log, then parse const text = await res.text(); try { console.debug('[CM] fetch body (json):', text.slice(0, 2000)); } catch {} try { return JSON.parse(text) as T; } catch { console.warn('[CM] failed to parse JSON, returning empty object'); return {} as T; } }, [base, getState] ); // Document templates const listTemplates = useCallback(async (): Promise => { const data = await authorizedFetch('/api/document-templates', { method: 'GET' }); return Array.isArray(data) ? data : []; }, [authorizedFetch]); const getTemplate = useCallback(async (id: string): Promise => { return authorizedFetch(`/api/document-templates/${id}`, { method: 'GET' }); }, [authorizedFetch]); const previewTemplateHtml = useCallback(async (id: string): Promise => { return authorizedFetch(`/api/document-templates/${id}/preview`, { method: 'GET' }, 'text'); }, [authorizedFetch]); const openPreviewInNewTab = useCallback(async (id: string) => { const html = await previewTemplateHtml(id); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank', 'noopener,noreferrer'); // No revoke here to keep the tab content; browser will clean up eventually }, [previewTemplateHtml]); const generatePdf = useCallback(async (id: string, opts?: { preview?: boolean; sanitize?: boolean }): Promise => { const params = new URLSearchParams(); if (opts?.preview) params.set('preview', '1'); if (opts?.sanitize) params.set('sanitize', '1'); const qs = params.toString() ? `?${params.toString()}` : ''; return authorizedFetch(`/api/document-templates/${id}/generate-pdf${qs}`, { method: 'GET' }, 'blob'); }, [authorizedFetch]); const downloadPdf = useCallback(async (id: string): Promise => { return authorizedFetch(`/api/document-templates/${id}/download-pdf`, { method: 'GET' }, 'blob'); }, [authorizedFetch]); const uploadTemplate = useCallback(async (payload: { file: File | Blob; name: string; type: string; contract_type?: 'contract' | 'gdpr' | 'abo'; lang: 'en' | 'de' | string; description?: string; user_type?: 'personal' | 'company' | 'both'; tax_mode?: 'standard' | 'reverse_charge' | 'both'; }): Promise => { const fd = new FormData(); const file = payload.file instanceof File ? payload.file : new File([payload.file], `${payload.name || 'template'}.html`, { type: 'text/html' }); fd.append('file', file); fd.append('name', payload.name); fd.append('type', payload.type); if (payload.type === 'contract' && payload.contract_type) { fd.append('contract_type', payload.contract_type); } if (payload.type === 'invoice' && payload.tax_mode) { fd.append('tax_mode', payload.tax_mode); } fd.append('lang', payload.lang); if (payload.description) fd.append('description', payload.description); fd.append('user_type', (payload.user_type ?? 'both')); try { console.info('[CM][document-templates] uploadTemplate()', { name: payload.name, type: payload.type, contract_type: payload.contract_type, willSendContractType: payload.type === 'contract' && Boolean(payload.contract_type), lang: payload.lang, user_type: payload.user_type ?? 'both', tax_mode: payload.type === 'invoice' ? (payload.tax_mode ?? 'both') : null, descriptionLength: payload.description ? payload.description.length : 0, file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null, }); } catch {} return authorizedFetch('/api/document-templates', { method: 'POST', body: fd }); }, [authorizedFetch]); const updateTemplate = useCallback(async (id: string, payload: { file?: File | Blob; name?: string; type?: string; contract_type?: 'contract' | 'gdpr' | 'abo'; lang?: 'en' | 'de' | string; description?: string; user_type?: 'personal' | 'company' | 'both'; tax_mode?: 'standard' | 'reverse_charge' | 'both'; }): Promise => { const fd = new FormData(); if (payload.file) { const f = payload.file instanceof File ? payload.file : new File([payload.file], `${payload.name || 'template'}.html`, { type: 'text/html' }); fd.append('file', f); } if (payload.name) fd.append('name', payload.name); if (payload.type) fd.append('type', payload.type); if ((payload.type === 'contract' || payload.contract_type) && payload.contract_type) { fd.append('contract_type', payload.contract_type); } if ((payload.type === 'invoice' || payload.tax_mode) && payload.tax_mode) { fd.append('tax_mode', payload.tax_mode); } if (payload.lang) fd.append('lang', payload.lang); if (payload.description !== undefined) fd.append('description', payload.description); if (payload.user_type) fd.append('user_type', payload.user_type); return authorizedFetch(`/api/document-templates/${id}`, { method: 'PUT', body: fd }); }, [authorizedFetch]); // NEW: revise template (create a new template record + bump version + deactivate previous) const reviseTemplate = useCallback(async (id: string, payload: { file: File | Blob; name?: string; type?: string; contract_type?: 'contract' | 'gdpr' | 'abo'; lang?: 'en' | 'de' | string; description?: string; user_type?: 'personal' | 'company' | 'both'; tax_mode?: 'standard' | 'reverse_charge' | 'both'; state?: 'active' | 'inactive'; }): Promise => { const fd = new FormData(); const file = payload.file instanceof File ? payload.file : new File([payload.file], `${payload.name || 'template'}.html`, { type: 'text/html' }); fd.append('file', file); if (payload.name !== undefined) fd.append('name', payload.name); if (payload.type !== undefined) fd.append('type', payload.type); if (payload.contract_type !== undefined) fd.append('contract_type', payload.contract_type); if (payload.tax_mode !== undefined) fd.append('tax_mode', payload.tax_mode); if (payload.lang !== undefined) fd.append('lang', payload.lang); if (payload.description !== undefined) fd.append('description', payload.description); if (payload.user_type !== undefined) fd.append('user_type', payload.user_type); if (payload.state !== undefined) fd.append('state', payload.state); try { console.info('[CM][document-templates] reviseTemplate()', { id, name: payload.name, type: payload.type, contract_type: payload.contract_type, lang: payload.lang, user_type: payload.user_type, tax_mode: payload.tax_mode, state: payload.state, descriptionLength: payload.description ? payload.description.length : 0, file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null, }); } catch {} return authorizedFetch(`/api/document-templates/${id}/revise`, { method: 'POST', body: fd }); }, [authorizedFetch]); const updateTemplateState = useCallback(async (id: string, state: 'active' | 'inactive'): Promise => { return authorizedFetch(`/api/document-templates/${id}/state`, { method: 'PATCH', body: JSON.stringify({ state }), }); }, [authorizedFetch]); const generatePdfWithSignature = useCallback(async (id: string, payload: { signatureImage?: string; signature?: string; signatureData?: string; userData?: Json; user?: Json; context?: Json; currentDate?: string; }): Promise => { const body: Json = {}; if (payload.signatureImage) body.signatureImage = payload.signatureImage; if (payload.signature) body.signature = payload.signature; if (payload.signatureData) body.signatureData = payload.signatureData; if (payload.userData) body.userData = payload.userData; if (payload.user) body.user = payload.user; if (payload.context) body.context = payload.context; if (payload.currentDate) body.currentDate = payload.currentDate; return authorizedFetch(`/api/document-templates/${id}/generate-pdf-with-signature`, { method: 'POST', body: JSON.stringify(body), }, 'blob'); }, [authorizedFetch]); // Helper: convert various base64 forms into a clean data URI const toDataUri = useCallback((raw: any, mime?: string | null): string | null => { if (!raw) return null; try { let s = String(raw); if (s.startsWith('data:')) return s; // already a data URI // Remove optional "base64," prefix s = s.replace(/^base64,/, ''); // Remove whitespace/newlines s = s.replace(/\s+/g, ''); // Convert URL-safe base64 to standard s = s.replace(/-/g, '+').replace(/_/g, '/'); // Pad to a multiple of 4 while (s.length % 4 !== 0) s += '='; const m = mime || 'image/png'; return `data:${m};base64,${s}`; } catch { return null; } }, []); // Helper: unwrap arrays from common API envelope shapes const unwrapList = useCallback((raw: any): any[] => { if (Array.isArray(raw)) return raw; if (Array.isArray(raw?.data)) return raw.data; if (Array.isArray(raw?.items)) return raw.items; if (Array.isArray(raw?.results)) return raw.results; return []; }, []); // Add image_base64 and other common variants const normalizeStamp = useCallback((s: any, forceActive = false): CompanyStamp => { const mime = s?.mime_type ?? s?.mimeType ?? s?.mimetype ?? s?.type ?? 'image/png'; const imgRaw = s?.image ?? s?.image_data ?? s?.image_base64 ?? // backend key seen in logs s?.imageBase64 ?? s?.base64 ?? s?.data ?? null; try { const presentKeys = Object.keys(s || {}).filter(k => ['image', 'image_data', 'image_base64', 'imageBase64', 'base64', 'data', 'mime_type', 'mimeType'].includes(k) ); console.debug('[CM] normalizeStamp keys:', presentKeys, 'hasImg:', !!imgRaw); } catch {} const dataUri = toDataUri(imgRaw, mime); return { id: s?.id ?? s?._id ?? s?.uuid ?? s?.stamp_id ?? String(Math.random()), label: s?.label ?? null, mimeType: mime, base64: dataUri, active: forceActive ? true : !!(s?.is_active ?? s?.active ?? s?.isActive), createdAt: s?.createdAt ?? s?.created_at, }; }, [toDataUri]); // New: fetch all stamps and the active one in one request const listStampsAll = useCallback(async (): Promise<{ stamps: CompanyStamp[]; active: CompanyStamp | null; activeId?: string | null; }> => { const raw = await authorizedFetch('/api/company-stamps/all', { method: 'GET' }); try { console.debug('[CM] /api/company-stamps/all raw:', { isArray: Array.isArray(raw), topKeys: raw && !Array.isArray(raw) ? Object.keys(raw) : [], dataKeys: raw?.data ? Object.keys(raw.data) : [], }); // Log first item keys to confirm field names like image_base64 const sample = Array.isArray(raw) ? raw[0] : (raw?.data?.[0] ?? raw?.items?.[0] ?? raw?.stamps?.[0]); if (sample) console.debug('[CM] /api/company-stamps/all sample keys:', Object.keys(sample)); } catch {} const container = raw?.data ?? raw; const rawList: any[] = Array.isArray(container?.items) ? container.items : Array.isArray(container?.stamps) ? container.stamps : Array.isArray(container?.list) ? container.list : Array.isArray(container) ? container : Array.isArray(raw?.items) ? raw.items : Array.isArray(raw) ? raw : []; const rawActive = container?.active ?? container?.current ?? container?.activeStamp ?? null; const stamps = rawList.map((s: any) => normalizeStamp(s)); let active = rawActive ? normalizeStamp(rawActive, true) : null; // Derive active from list if not provided separately if (!active) { const fromList = stamps.find(s => s.active); if (fromList) active = { ...fromList, active: true }; } // Mark the active in stamps if present const activeId = active?.id ?? (container?.active_id ?? container?.activeId ?? null); const stampsMarked = activeId ? stamps.map((s) => (s.id === activeId ? { ...s, active: true, base64: s.base64 || active?.base64, mimeType: s.mimeType || active?.mimeType } : s)) : stamps; try { console.debug('[CM] /api/company-stamps/all normalized:', { total: stampsMarked.length, withImg: stampsMarked.filter(s => !!s.base64).length, activeId: activeId || active?.id || null, hasActiveImg: !!active?.base64, }); } catch {} return { stamps: stampsMarked, active, activeId: activeId || active?.id || null }; }, [authorizedFetch, normalizeStamp]); const listMyCompanyStamps = useCallback(async (): Promise => { const { stamps } = await listStampsAll(); return stamps; }, [listStampsAll]); const getActiveCompanyStamp = useCallback(async (): Promise => { const { active } = await listStampsAll(); return active ?? null; }, [listStampsAll]); // helper: convert File/Blob to base64 string and mime const fileToBase64 = useCallback( (file: File | Blob) => new Promise<{ base64: string; mime: string }>((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error('Failed to read file')); reader.onload = () => { const result = String(reader.result || ''); const m = result.match(/^data:(.+?);base64,(.*)$/); if (m) { resolve({ mime: m[1], base64: m[2] }); } else { resolve({ mime: (file as File).type || 'application/octet-stream', base64: result }); } }; reader.readAsDataURL(file); }), [] ); // Upload expects JSON { base64, mime_type/mimeType, label? } const uploadCompanyStamp = useCallback(async (file: File | Blob, label?: string) => { const { base64, mime } = await fileToBase64(file); try { console.debug('[CM] uploadCompanyStamp payload:', { mime, base64Len: base64?.length || 0, hasLabel: !!label }); } catch {} return authorizedFetch('/api/company-stamps', { method: 'POST', body: JSON.stringify({ base64, mimeType: mime, mime_type: mime, ...(label ? { label } : {}), }), }); }, [authorizedFetch, fileToBase64]); const activateCompanyStamp = useCallback(async (id: string) => { console.debug('[CM] activateCompanyStamp ->', id); return authorizedFetch<{ success?: boolean }>(`/api/company-stamps/${id}/activate`, { method: 'PATCH' }); }, [authorizedFetch]); const deactivateCompanyStamp = useCallback(async (id: string) => { console.debug('[CM] deactivateCompanyStamp ->', id); return authorizedFetch<{ success?: boolean }>(`/api/company-stamps/${id}/deactivate`, { method: 'PATCH' }); }, [authorizedFetch]); // Delete a company stamp (strict: no fallback) const deleteCompanyStamp = useCallback(async (id: string) => { console.debug('[CM] deleteCompanyStamp ->', id); return authorizedFetch<{ success?: boolean }>(`/api/company-stamps/${id}`, { method: 'DELETE' }); }, [authorizedFetch]); const downloadBlobFile = useCallback((blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, []); const listTemplatesPublic = useCallback(async (): Promise => { const data = await authorizedFetch('/api/document-templates/public', { method: 'GET' }); return Array.isArray(data) ? data : []; }, [authorizedFetch]); // Company settings (invoice address info) type CompanySettings = { company_name?: string company_street?: string company_postal_city?: string company_country?: string company_logo_base64?: string | null company_logo_mime_type?: string | null } const getCompanySettings = useCallback(async () => { return authorizedFetch('/api/admin/company-settings', { method: 'GET' }); }, [authorizedFetch]); const updateCompanySettings = useCallback(async (data: Partial) => { return authorizedFetch('/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data), }); }, [authorizedFetch]); return { // templates listTemplates, getTemplate, previewTemplateHtml, openPreviewInNewTab, generatePdf, downloadPdf, uploadTemplate, reviseTemplate, updateTemplate, updateTemplateState, generatePdfWithSignature, listTemplatesPublic, // stamps listStampsAll, listMyCompanyStamps, getActiveCompanyStamp, uploadCompanyStamp, activateCompanyStamp, deactivateCompanyStamp, deleteCompanyStamp, // company settings getCompanySettings, updateCompanySettings, // utils downloadBlobFile, }; }