625 lines
23 KiB
TypeScript
625 lines
23 KiB
TypeScript
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;
|
|
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<string, any>;
|
|
|
|
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<string, any> = {};
|
|
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<string, any> = {};
|
|
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 <T = any>(
|
|
path: string,
|
|
init: RequestInit = {},
|
|
responseType: 'json' | 'text' | 'blob' = 'json'
|
|
): Promise<T> => {
|
|
let token = getState().accessToken;
|
|
if (!token) {
|
|
const ok = await getState().refreshAuthToken();
|
|
if (ok) token = getState().accessToken;
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
...(init.headers as Record<string, string> || {}),
|
|
...(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<string, any>;
|
|
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<DocumentTemplate[]> => {
|
|
const data = await authorizedFetch<DocumentTemplate[]>('/api/document-templates', { method: 'GET' });
|
|
return Array.isArray(data) ? data : [];
|
|
}, [authorizedFetch]);
|
|
|
|
const getTemplate = useCallback(async (id: string): Promise<DocumentTemplate> => {
|
|
return authorizedFetch<DocumentTemplate>(`/api/document-templates/${id}`, { method: 'GET' });
|
|
}, [authorizedFetch]);
|
|
|
|
const previewTemplateHtml = useCallback(async (id: string): Promise<string> => {
|
|
return authorizedFetch<string>(`/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<Blob> => {
|
|
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<Blob>(`/api/document-templates/${id}/generate-pdf${qs}`, { method: 'GET' }, 'blob');
|
|
}, [authorizedFetch]);
|
|
|
|
const downloadPdf = useCallback(async (id: string): Promise<Blob> => {
|
|
return authorizedFetch<Blob>(`/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';
|
|
}): Promise<DocumentTemplate> => {
|
|
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);
|
|
}
|
|
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',
|
|
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<DocumentTemplate>('/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';
|
|
}): Promise<DocumentTemplate> => {
|
|
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.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<DocumentTemplate>(`/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';
|
|
state?: 'active' | 'inactive';
|
|
}): Promise<DocumentTemplate> => {
|
|
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.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,
|
|
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<DocumentTemplate>(`/api/document-templates/${id}/revise`, { method: 'POST', body: fd });
|
|
}, [authorizedFetch]);
|
|
|
|
const updateTemplateState = useCallback(async (id: string, state: 'active' | 'inactive'): Promise<DocumentTemplate> => {
|
|
return authorizedFetch<DocumentTemplate>(`/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<Blob> => {
|
|
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<Blob>(`/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<any>('/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<CompanyStamp[]> => {
|
|
const { stamps } = await listStampsAll();
|
|
return stamps;
|
|
}, [listStampsAll]);
|
|
|
|
const getActiveCompanyStamp = useCallback(async (): Promise<CompanyStamp | null> => {
|
|
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<CompanyStamp>('/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<DocumentTemplate[]> => {
|
|
const data = await authorizedFetch<DocumentTemplate[]>('/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
|
|
// NEW: QR codes for invoices (base64 or data URL)
|
|
qr_code_60_base64?: string | null
|
|
qr_code_120_base64?: string | null
|
|
// NEW: allow camelCase too (backend supports both)
|
|
qrCode60Base64?: string | null
|
|
qrCode120Base64?: string | null
|
|
}
|
|
|
|
const getCompanySettings = useCallback(async () => {
|
|
return authorizedFetch<CompanySettings>('/api/admin/company-settings', { method: 'GET' });
|
|
}, [authorizedFetch]);
|
|
|
|
const updateCompanySettings = useCallback(async (data: Partial<CompanySettings>) => {
|
|
// Debug request body in browser console (redacts base64 values)
|
|
try {
|
|
// IMPORTANT: `data` is the real payload object; `redacted` is for logs only.
|
|
const json = JSON.stringify(data);
|
|
const redacted = redactJsonForLogs(data);
|
|
const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64;
|
|
const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64;
|
|
console.info('[CM][company-settings] PUT body', {
|
|
redacted,
|
|
jsonLength: json.length,
|
|
keys: Object.keys(data || {}),
|
|
qrFieldTypes: {
|
|
qr_code_60_base64: qr60 ? typeof qr60 : null,
|
|
qr_code_120_base64: qr120 ? typeof qr120 : null,
|
|
},
|
|
qrFieldLengths: {
|
|
qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null,
|
|
qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null,
|
|
},
|
|
});
|
|
|
|
if (qr60 && typeof qr60 !== 'string') {
|
|
console.warn('[CM][company-settings] qr_code_60_base64 is not a string!', qr60);
|
|
}
|
|
if (qr120 && typeof qr120 !== 'string') {
|
|
console.warn('[CM][company-settings] qr_code_120_base64 is not a string!', qr120);
|
|
}
|
|
} catch {}
|
|
|
|
return authorizedFetch<CompanySettings>('/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,
|
|
};
|
|
}
|