// Piezas visuales del DocumentEditor: secciones, sección plegable, tarjeta de
// contraparte, totales, revisión y vista previa. Sin estado de negocio: el
// editor (o un remote federado) les pasa los datos ya calculados. Los contratos
// de datos viven en primitives/panels.ts.
import { useId, useState, type ReactNode } from 'react'
import { Badge } from '@asteby/metacore-ui/primitives'
import { AlertTriangle, ChevronDown, XCircle } from 'lucide-react'
import type { CreditStatus, EditorIssue } from './document-editor-model'
import type { Formatter } from './format'
import { computeLine, type LineItem } from './line-items'
/** Bloque con título discreto y espaciado generoso (sin bordes pesados). */
export function EditorSection({ title, hint, children, slot }: { title?: ReactNode; hint?: ReactNode; children: ReactNode; slot?: string }) {
return (
{(title || hint) && (
{title &&
{title}
}
{hint && {hint}}
)}
{children}
)
}
export interface CollapsibleSectionProps {
title: ReactNode
/** Resumen de una línea cuando está cerrada (p. ej. «G03 · PUE»). */
summary?: ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
children: ReactNode
slot?: string
}
/**
* Sección plegable. El contenido queda MONTADO aunque esté cerrada (`hidden`):
* las contribuciones de otros addons siguen aplicando defaults y reportando
* revisiones sin que el usuario abra la sección.
*/
export function CollapsibleSection({ title, summary, open, defaultOpen = false, onOpenChange, children, slot }: CollapsibleSectionProps) {
const [inner, setInner] = useState(defaultOpen)
const isOpen = open ?? inner
const id = useId()
const toggle = () => {
setInner(!isOpen)
onOpenChange?.(!isOpen)
}
return (
{children}
)
}
export interface PartyCardProps {
name?: string
rows: Array<{ key: string; label: string; value: string }>
credit?: CreditStatus | null
fmt: Formatter
children?: ReactNode
}
/** Tarjeta compacta de la contraparte: nombre, 3-6 datos clave y estado de crédito. */
export function PartyCard({ name, rows, credit, fmt, children }: PartyCardProps) {
return (
)
}
/** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */
export function ValidationChecklist({ issues, title = 'Revisa antes de guardar' }: { issues: EditorIssue[]; title?: string }) {
if (issues.length === 0) return null
return (
{title}
{issues.map((i, k) => (
{i.severity === 'error' ? : }
{i.message}
))}
)
}
export interface PreviewContent {
xml?: string
html?: string
pdf_url?: string
pdf_base64?: string
}
/** Contenido de la vista previa (PDF, HTML o XML, en ese orden de preferencia). */
export function PreviewPanel({ preview }: { preview: PreviewContent | null }) {
if (!preview) return null
const pdf = preview.pdf_url ?? (preview.pdf_base64 ? `data:application/pdf;base64,${preview.pdf_base64}` : undefined)
if (pdf) return
if (preview.html) return
if (preview.xml) return
{preview.xml}
return null
}
export interface DraftPreviewProps {
title: string
party?: string
/** Datos del encabezado ya con su etiqueta y valor legibles. */
header: Array<{ label: string; value: string }>
lines: LineItem[]
totals: TotalsRow[]
fmt: Formatter
}
/**
* Vista previa local del documento (sin servidor ni borrador): cómo queda con
* lo capturado. Para tipos sin acción `preview`; la del servidor (PDF/XML) gana.
*/
export function DraftPreview({ title, party, header, lines, totals, fmt }: DraftPreviewProps) {
const items = lines.filter((l) => l.kind === 'item')
return (
{title}
{party && {party}}
{header.length > 0 && (