b311ab7aa1
Check Cross-Plugin Imports / check (push) Has been cancelled
- 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
297 lines
9.5 KiB
TypeScript
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 });
|
|
}
|