Files
leocrm/frontend/src/api/documents.ts
T
Agent Zero b311ab7aa1
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(L1-L3): Dokumente-Generator — Briefpapier+Block-System+Drag&Drop-Editor+Renderer
- Briefpapier (letterheads): Seiten-Setup (A4/A5/Letter, Ränder), Header/Footer-Blöcke, Wasserzeichen, Logo-Upload (DocumentAsset, data:-URI-only)
- Druckvorlagen (print_templates): Block-Komposition mit Briefpapier-Ref + entity_type
- Block-Registry (document_blocks.py): text/image/shape(line/rect/circle)/table/spacer/divider/placeholder/pagebreak + Modul-Beiträge via document_blocks()-Contract
- Renderer (document_renderer.py): Blocks→HTML→PDF via WeasyPrint (SSRF-Sandbox data:-URI-only), @page-Frame mit running header/footer, Placeholder-Beispiel-Defaults gegen StrictUndefined
- Contract-Beitrag contacts: document_placeholders/document_data (#359-Muster wie importexport_entities)
- 13 neue Endpoints in documents.py: Letterhead-CRUD, Template-CRUD, Assets, document-blocks, document-placeholders, preview (HTML), render (PDF)
- Migration: Plugin-SQL 0003 (idempotent) + Alembic 0143 (Dual-Path, RLS fail-closed crm_api)
- Frontend: api/documents.ts, Settings→Dokumente (settings_pages), BlockEditor (@dnd-kit Palette/Canvas/Config/Live-Preview-iframe), LetterheadEditor, PrintTemplateEditor, DocumentGenerationDialog (global, ContactDetailPage-Integration)
- i18n de/en, api-documentation.md, plugin-development-guide.md, PROGRESS.md

Verifikation: 32/32 neue Tests + 9/9 Regressionen, tsc exit 0, Build OK 2.79s, Alembic-Fresh-DB 0143 mit RLS bewiesen, ruff clean
2026-08-29 09:49:16 +02:00

297 lines
9.5 KiB
TypeScript

/**
* Documents Generator API client (Phase L1-L3).
*
* Letterheads (Briefpapier), print templates (block compositions),
* block registry and placeholders — all under /reports/... (documents
* router of the report_generator plugin).
*/
import { apiClient, apiDelete, apiGet, apiPost, apiPut } from './client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
// ─── Types ─────────────────────────────────────────────────────────────────
export interface DocBlock {
id: string;
type: string;
config: Record<string, unknown>;
}
export interface LetterheadPageConfig {
size?: string;
orientation?: string;
margins?: { top?: number; right?: number; bottom?: number; left?: number };
}
export interface LetterheadConfig {
page?: LetterheadPageConfig;
header?: { enabled: boolean; blocks: DocBlock[] };
footer?: { enabled: boolean; blocks: DocBlock[] };
watermark?: { enabled: boolean; text?: string };
}
export interface Letterhead {
id: string;
name: string;
description: string;
config: LetterheadConfig;
is_default: boolean;
created_by: string;
created_at?: string | null;
updated_at?: string | null;
}
export interface PrintTemplate {
id: string;
name: string;
description: string;
letterhead_id: string | null;
entity_type: string;
blocks: DocBlock[];
output_format: string;
created_by: string;
created_at?: string | null;
updated_at?: string | null;
}
export interface DocumentBlockType {
type: string;
label: string;
category: string;
description: string;
fields: Record<string, string>;
builtin: boolean;
contributed_by?: string | null;
}
export interface DocumentPlaceholder {
key: string;
label: string;
example: string;
}
export interface DocumentAsset {
id: string;
letterhead_id: string | null;
filename: string;
mime_type: string;
size_bytes: number;
data_url: string | null;
created_at?: string | null;
}
export interface LetterheadInput {
name: string;
description?: string;
config?: LetterheadConfig;
is_default?: boolean;
}
export interface PrintTemplateInput {
name: string;
description?: string;
letterhead_id?: string | null;
entity_type?: string;
blocks?: DocBlock[];
output_format?: string;
}
// ─── Query Keys ─────────────────────────────────────────────────────────────
export const DOC_QUERY_KEYS = {
letterheads: ['documents', 'letterheads'] as const,
letterhead: (id: string) => ['documents', 'letterheads', id] as const,
templates: ['documents', 'print-templates'] as const,
template: (id: string) => ['documents', 'print-templates', id] as const,
blocks: ['documents', 'block-types'] as const,
placeholders: (entityType?: string) => ['documents', 'placeholders', entityType ?? 'all'] as const,
assets: (letterheadId: string) => ['documents', 'assets', letterheadId] as const,
};
// ─── Fetch helpers ─────────────────────────────────────────────────────────
async function fetchLetterheads(): Promise<Letterhead[]> {
const res = await apiGet<{ items: Letterhead[]; total: number }>('/reports/letterheads');
return res.items ?? [];
}
async function fetchTemplates(): Promise<PrintTemplate[]> {
const res = await apiGet<{ items: PrintTemplate[]; total: number }>('/reports/print-templates');
return res.items ?? [];
}
async function fetchBlockTypes(): Promise<DocumentBlockType[]> {
return apiGet<DocumentBlockType[]>('/reports/document-blocks');
}
async function fetchPlaceholders(entityType?: string): Promise<Record<string, DocumentPlaceholder[]>> {
const url = entityType
? `/reports/document-placeholders?entity_type=${encodeURIComponent(entityType)}`
: '/reports/document-placeholders';
return apiGet<Record<string, DocumentPlaceholder[]>>(url);
}
async function fetchAssets(letterheadId: string): Promise<DocumentAsset[]> {
return apiGet<DocumentAsset[]>(`/reports/letterheads/${letterheadId}/assets`);
}
// ─── Hooks: Letterheads ─────────────────────────────────────────────────────
export function useLetterheads() {
return useQuery<Letterhead[]>({
queryKey: DOC_QUERY_KEYS.letterheads,
queryFn: fetchLetterheads,
});
}
export function useCreateLetterhead() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: LetterheadInput) =>
apiPost<Letterhead>('/reports/letterheads', input),
onSuccess: () => qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.letterheads }),
});
}
export function useUpdateLetterhead() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, input }: { id: string; input: Partial<LetterheadInput> }) =>
apiPut<Letterhead>(`/reports/letterheads/${id}`, input),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.letterheads });
qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.letterhead(vars.id) });
},
});
}
export function useDeleteLetterhead() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiDelete(`/reports/letterheads/${id}`),
onSuccess: () => qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.letterheads }),
});
}
// ─── Hooks: Print Templates ─────────────────────────────────────────────────
export function usePrintTemplates() {
return useQuery<PrintTemplate[]>({
queryKey: DOC_QUERY_KEYS.templates,
queryFn: fetchTemplates,
});
}
export function useCreatePrintTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: PrintTemplateInput) =>
apiPost<PrintTemplate>('/reports/print-templates', input),
onSuccess: () => qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.templates }),
});
}
export function useUpdatePrintTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, input }: { id: string; input: Partial<PrintTemplateInput> }) =>
apiPut<PrintTemplate>(`/reports/print-templates/${id}`, input),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.templates });
qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.template(vars.id) });
},
});
}
export function useDeletePrintTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiDelete(`/reports/print-templates/${id}`),
onSuccess: () => qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.templates }),
});
}
// ─── Hooks: Registry ────────────────────────────────────────────────────────
export function useDocumentBlockTypes() {
return useQuery<DocumentBlockType[]>({
queryKey: DOC_QUERY_KEYS.blocks,
queryFn: fetchBlockTypes,
staleTime: 5 * 60 * 1000,
});
}
export function useDocumentPlaceholders(entityType?: string, enabled = true) {
return useQuery<Record<string, DocumentPlaceholder[]>>({
queryKey: DOC_QUERY_KEYS.placeholders(entityType),
queryFn: () => fetchPlaceholders(entityType),
enabled,
staleTime: 5 * 60 * 1000,
});
}
export function useLetterheadAssets(letterheadId: string | null | undefined) {
return useQuery<DocumentAsset[]>({
queryKey: DOC_QUERY_KEYS.assets(letterheadId ?? 'none'),
queryFn: () => fetchAssets(letterheadId as string),
enabled: !!letterheadId,
});
}
export function useUploadLetterheadAsset() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ letterheadId, file }: { letterheadId: string; file: File }) => {
const form = new FormData();
form.append('file', file);
return apiClient
.post(`/reports/letterheads/${letterheadId}/assets`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((r) => r.data as DocumentAsset);
},
onSuccess: (_data, vars) =>
qc.invalidateQueries({ queryKey: DOC_QUERY_KEYS.assets(vars.letterheadId) }),
});
}
// ─── Preview & Render ──────────────────────────────────────────────────────
export interface PreviewInput {
blocks: DocBlock[];
letterhead_config: LetterheadConfig | null;
entity_type?: string | null;
data?: Record<string, unknown> | null;
}
export function usePreviewDocument() {
return useMutation({
mutationFn: (input: PreviewInput) =>
apiPost<{ html: string }>('/reports/documents/preview', input),
});
}
export interface RenderInput {
templateId: string;
entityType: string;
entityId: string;
outputFormat?: string;
}
export async function renderPrintTemplate(input: RenderInput): Promise<Blob> {
const response = await apiClient.post(
`/reports/print-templates/${input.templateId}/render`,
{
entity_type: input.entityType,
entity_id: input.entityId,
output_format: input.outputFormat ?? 'pdf',
},
{ responseType: 'blob' },
);
return response.data as Blob;
}
export function useRenderPrintTemplate() {
return useMutation({ mutationFn: renderPrintTemplate });
}