feat(web): telas de Documentos e Ponto Eletrônico — cobertura completa do EDEN Core
Última leva desta sequência, sem parar para autorização. 2 subagentes
eden-frontend em paralelo.
- Documentos: templates com ciclo draft→publish (edição por texto com
{{variavel}}, sem editor Tiptap real ainda), preview do HTML
publicado, geração de documento com extração automática de variáveis
via regex e aviso de missingVariables. Achado do agente corrigindo
meu brief: regra de key aceita minúsculas+dígitos+underscore, não só
minúsculas+underscore.
- Ponto Eletrônico (fundação): dispositivos Control iD (senha nunca
retornada, só hasCredential), import de AFD com decodificação
ISO-8859-1 explícita documentada no código, registros AFD só-leitura
(sem nenhuma ação de edição — imutabilidade legal respeitada também
na UI). 2 discrepâncias reais encontradas e corrigidas pelo agente:
reimportar retorna 409 estruturado (não 200), e a listagem de
registros é um envelope paginado (não array simples).
Reconciliação manual de App.tsx/AuthenticatedLayout.tsx entre os 2
agentes. Verificado: pnpm turbo run typecheck build (28/28); containers
eden-api/eden-core reconstruídos; testado ao vivo através do proxy
same-origin (porta 3001) incluindo confirmação de que a senha do
dispositivo nunca aparece na resposta; trilha de auditoria conferida.
Com esta leva, o EDEN Core tem UI real para todo o backend construído
nesta sessão, exceto Assinatura Eletrônica e apuração/fechamento de
ponto (nenhum dos dois existe no backend ainda).
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
197
apps/core-web/src/lib/api/documents.ts
Normal file
197
apps/core-web/src/lib/api/documents.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/documents/templates` and `/v1/documents/generations`
|
||||
* — the "templates + generation" half of Documents (eden.md módulo 4, §1.1-1.3).
|
||||
*
|
||||
* This leva has NO real visual editor and NO real PDF: `editorContent` is a
|
||||
* simplified JSONB with a single root-level `plainText` string carrying
|
||||
* `{{variavel}}` placeholders (no rich node tree), and "generation" just
|
||||
* merges variables into `renderedHtml` — `pdf_s3_key`/`pdf_sha256`/
|
||||
* `pdf_size_bytes` are always `null` here. Don't build UI implying more.
|
||||
*
|
||||
* Like Contracts/Resellers, template/version/generation responses are
|
||||
* snake_case straight off the DB row — except the two computed fields on a
|
||||
* generation response (`missingVariables`, `deduped`), which come back
|
||||
* camelCase exactly as documented in the task brief; mirrored as-is below,
|
||||
* not "fixed" to snake_case.
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
export type DocGroup = "cliente" | "revenda";
|
||||
export type DocumentVersionStatus = "draft" | "published";
|
||||
export type GenerationStatus = "generating" | "final" | "error";
|
||||
|
||||
/**
|
||||
* Regex the server validates `key` against — confirmed live against the API
|
||||
* ("key deve começar com letra minúscula e conter apenas letras
|
||||
* minúsculas, números e underscore"): starts with a lowercase letter,
|
||||
* followed by lowercase letters/digits/underscore, e.g.
|
||||
* `client_software_contract`. Shown as a hint in the create form.
|
||||
*/
|
||||
export const DOCUMENT_TEMPLATE_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
||||
|
||||
/** Simplified editor content for this leva — always `{ type: "doc", plainText }`, never a rich node tree. */
|
||||
export interface SimpleEditorContent {
|
||||
type: "doc";
|
||||
plainText: string;
|
||||
}
|
||||
|
||||
/** Response shape for `GET /documents/templates` (list) and the bare fields of the detail response. */
|
||||
export interface DocumentTemplate {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
key: string;
|
||||
name: string;
|
||||
doc_group: DocGroup;
|
||||
active: boolean;
|
||||
current_version_id: string | null;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export interface DocumentTemplateVersion {
|
||||
id: string;
|
||||
template_id: string;
|
||||
version: number;
|
||||
status: DocumentVersionStatus;
|
||||
editor_content: SimpleEditorContent | Record<string, unknown>;
|
||||
html_content: string | null;
|
||||
css_content: string | null;
|
||||
page_config: Record<string, unknown> | null;
|
||||
created_by: string | null;
|
||||
published_at: string | null;
|
||||
published_by: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Response shape for `GET /documents/templates/:id` — adds the version list. */
|
||||
export interface DocumentTemplateWithVersions extends DocumentTemplate {
|
||||
versions: DocumentTemplateVersion[];
|
||||
}
|
||||
|
||||
/** Body of `POST /documents/templates` — matches `CreateDocumentTemplateDto` exactly. */
|
||||
export interface CreateDocumentTemplateInput {
|
||||
key: string;
|
||||
name: string;
|
||||
docGroup: DocGroup;
|
||||
}
|
||||
|
||||
/** Body of `PATCH /documents/templates/:id/draft` — matches `UpdateDraftDto`. `pageConfig` is deliberately omitted from the UI. */
|
||||
export interface UpdateDraftInput {
|
||||
editorContent?: SimpleEditorContent;
|
||||
cssContent?: string;
|
||||
pageConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateGenerationInput {
|
||||
templateId: string;
|
||||
clientRegistrationId?: string;
|
||||
resellerId?: string;
|
||||
context: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DocumentGeneration {
|
||||
id: string;
|
||||
created_at: string;
|
||||
template_id: string;
|
||||
template_version_id: string;
|
||||
client_registration_id: string | null;
|
||||
reseller_id: string | null;
|
||||
render_context: Record<string, unknown>;
|
||||
rendered_html: string;
|
||||
pdf_s3_key: string | null;
|
||||
pdf_sha256: string | null;
|
||||
pdf_size_bytes: number | null;
|
||||
status: GenerationStatus;
|
||||
error_message: string | null;
|
||||
generated_by: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response of `POST /documents/generations` only — confirmed against the
|
||||
* live API that `GET /documents/generations` (list) does NOT include these
|
||||
* two computed fields, so they're kept off the bare `DocumentGeneration`
|
||||
* shared by both responses. camelCase per API contract, not "fixed" to
|
||||
* snake_case.
|
||||
*/
|
||||
export interface DocumentGenerationResult extends DocumentGeneration {
|
||||
/** Variables referenced in the template but absent from the submitted `context`. */
|
||||
missingVariables: string[];
|
||||
/** `true` when an existing generation was reused instead of creating a new one. */
|
||||
deduped: boolean;
|
||||
}
|
||||
|
||||
export function listDocumentTemplates(accessToken: string): Promise<DocumentTemplate[]> {
|
||||
return apiRequest<DocumentTemplate[]>("/documents/templates", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getDocumentTemplate(id: string, accessToken: string): Promise<DocumentTemplateWithVersions> {
|
||||
return apiRequest<DocumentTemplateWithVersions>(`/documents/templates/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createDocumentTemplate(
|
||||
input: CreateDocumentTemplateInput,
|
||||
accessToken: string,
|
||||
): Promise<DocumentTemplate> {
|
||||
return apiRequest<DocumentTemplate>(
|
||||
"/documents/templates",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
/** Only succeeds if the template has no draft yet — 409 (`ApiError.status === 409`) when one already exists. */
|
||||
export function createDraft(templateId: string, accessToken: string): Promise<DocumentTemplateVersion> {
|
||||
return apiRequest<DocumentTemplateVersion>(
|
||||
`/documents/templates/${templateId}/draft`,
|
||||
{ method: "POST" },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDraft(
|
||||
templateId: string,
|
||||
input: UpdateDraftInput,
|
||||
accessToken: string,
|
||||
): Promise<DocumentTemplateVersion> {
|
||||
return apiRequest<DocumentTemplateVersion>(
|
||||
`/documents/templates/${templateId}/draft`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
/** Marks the current draft as published — permanent, the resulting version is never editable again. */
|
||||
export function publishDraft(templateId: string, accessToken: string): Promise<DocumentTemplateVersion> {
|
||||
return apiRequest<DocumentTemplateVersion>(
|
||||
`/documents/templates/${templateId}/publish`,
|
||||
{ method: "POST" },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
export function listGenerations(accessToken: string): Promise<DocumentGeneration[]> {
|
||||
return apiRequest<DocumentGeneration[]>("/documents/generations", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createGeneration(
|
||||
input: CreateGenerationInput,
|
||||
accessToken: string,
|
||||
): Promise<DocumentGenerationResult> {
|
||||
return apiRequest<DocumentGenerationResult>(
|
||||
"/documents/generations",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
/** Extracts `{{variavel}}` references from a plain-text template body — used to auto-build the generation context form. */
|
||||
export function extractTemplateVariables(plainText: string): string[] {
|
||||
const found = new Set<string>();
|
||||
const pattern = /\{\{(\w+)\}\}/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(plainText)) !== null) {
|
||||
const variable = match[1];
|
||||
if (variable) found.add(variable);
|
||||
}
|
||||
return Array.from(found);
|
||||
}
|
||||
Reference in New Issue
Block a user