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:
2026-09-04 06:14:23 -03:00
parent 828e6e8ddb
commit 012c00fa4d
17 changed files with 2577 additions and 0 deletions

View 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);
}