Files
mini-erp/mini-frontend/src/components/report/HtmlReportRunner.tsx
T
George fe22681c66 Personalizacion cliente tagumedica: permisos, refactor de vistas y limpieza de tools
- Agrega permiso de menu para vania.barrios@tagumedica.com
- Renombra test/ a tools/ (colecciones de requests SAP/ApiGateway)
- Renombra vistas de frontend con sufijo View para consistencia
- Refactor de componentes UI compartidos y agrega utilidades (Icon, PageLoader, SecondaryButton, FieldTooltip, errors)
2026-09-04 09:24:31 -05:00

272 lines
9.7 KiB
TypeScript

import { useState, useCallback } from "react";
import { useParams, Link } from "react-router-dom";
import { API_URL, apiFetch } from "../../utils/api";
import { htmlReportsConfig } from "../../config/htmlReports";
import ModalSearchTemplate from "../ui/ModalSearchTemplate";
import Icon from "../ui/Icon";
import type { F4Config } from "../../types/reports";
export default function HtmlReportRunner() {
const { module: moduleKey, docCode } = useParams<{ module: string; docCode: string }>();
const definition = htmlReportsConfig[moduleKey || ""]?.find((r) => r.id === docCode);
const parameters = definition?.parameters || [];
const [paramValues, setParamValues] = useState<Record<string, any>>(() => {
const initial: Record<string, any> = {};
for (const p of parameters) {
initial[p.name] = p.defaultValue ?? "";
}
return initial;
});
const [f4State, setF4State] = useState<{
param: (typeof parameters)[number];
config: F4Config;
} | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [previewUrl, setPreviewUrl] = useState("");
const handleChange = useCallback((name: string, value: any) => {
setParamValues((prev) => ({ ...prev, [name]: value }));
}, []);
const handleF4Confirm = useCallback(
(rows: any[]) => {
if (!f4State) return;
const { param, config } = f4State;
handleChange(param.name, rows[0]?.[config.valueField] ?? "");
setF4State(null);
},
[f4State, handleChange],
);
const handleGenerate = async () => {
if (!definition) return;
const body: Record<string, any> = {};
for (const p of parameters) {
const raw = paramValues[p.name];
body[p.name] = p.inputType === "number" ? Number(raw) : raw;
}
setLoading(true);
setError("");
setPreviewUrl("");
try {
const res = await apiFetch(`${API_URL}/api/reports/${definition.endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const errData = await res.json().catch(() => null);
throw new Error(errData?.detail || "Error al generar el reporte");
}
const blob = await res.blob();
setPreviewUrl(URL.createObjectURL(blob));
} catch (err: any) {
setError(err.message || "Error de conexión");
} finally {
setLoading(false);
}
};
const renderField = (param: (typeof parameters)[number]) => {
const val = paramValues[param.name] ?? "";
const required = param.required;
switch (param.inputType) {
case "text":
return (
<div>
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
{param.label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
<input
type="text"
value={val}
placeholder={param.placeholder}
onChange={(e) => handleChange(param.name, e.target.value)}
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-[var(--color-surface)] outline-none focus:border-[var(--color-input-focus)] text-xs"
/>
</div>
);
case "number":
return (
<div>
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
{param.label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
<input
type="number"
value={val}
placeholder={param.placeholder}
onChange={(e) => handleChange(param.name, e.target.value)}
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-[var(--color-surface)] outline-none focus:border-[var(--color-input-focus)] text-xs"
/>
</div>
);
case "date":
return (
<div>
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
{param.label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
<input
type="date"
value={val}
onChange={(e) => handleChange(param.name, e.target.value)}
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-[var(--color-surface)] outline-none focus:border-[var(--color-input-focus)] text-xs"
/>
</div>
);
case "select":
return (
<div>
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
{param.label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
<select
value={val}
onChange={(e) => handleChange(param.name, e.target.value)}
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-[var(--color-surface)] outline-none focus:border-[var(--color-input-focus)] text-xs"
>
{(param.options || []).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
case "f4-modal": {
const config = param.f4Config;
if (!config) return null;
const displayVal = val ? String(val) : "";
return (
<div>
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
{param.label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
<div className="flex items-center border border-[var(--color-input-border)] bg-[var(--color-surface)] focus-within:border-[var(--color-input-focus)]">
<input
type="text"
value={displayVal}
readOnly
placeholder={param.placeholder || "Seleccionar..."}
className="flex-1 min-w-0 h-[28px] px-2 border-none outline-none bg-transparent text-xs text-[var(--color-text)] cursor-pointer"
onClick={() => setF4State({ param, config })}
/>
<button
type="button"
onClick={() => setF4State({ param, config })}
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-[var(--color-primary)] shrink-0"
title="Buscar"
>
<Icon name="search" className="w-4 h-4" />
</button>
</div>
</div>
);
}
default:
return null;
}
};
return (
<div className="p-4">
<div className="mb-4">
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)] mb-1">
<Link
to={`/reportes-html/${moduleKey}`}
className="decoration-transparent text-[var(--color-primary)] hover:underline"
>
Reportes HTML
</Link>
<span>/</span>
<span className="text-[var(--color-sidebar-title)]">
{definition?.title || docCode}
</span>
</div>
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
<Icon name={definition?.icon || "document"} className="w-5 h-5" /> {definition?.title || docCode}
</h1>
</div>
{error && (
<div className="text-xs text-[var(--color-error)] bg-[var(--color-error-bg)] border border-[var(--color-error-border)] p-2 mb-4">
{error}
</div>
)}
{parameters.length > 0 && (
<div className="border border-[var(--color-border)] bg-[var(--color-bg)] p-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{parameters.map((p) => (
<div key={p.name} className="min-w-0">
{renderField(p)}
</div>
))}
</div>
<div className="flex gap-2 pt-3 border-t border-[var(--color-border)] mt-3">
<button
onClick={handleGenerate}
disabled={loading}
className="px-5 py-1.5 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white font-medium rounded-none border-none cursor-pointer text-xs disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Generando..." : "Generar Reporte"}
</button>
</div>
</div>
)}
{parameters.length === 0 && (
<div className="text-xs text-[var(--color-text-muted)] p-6 text-center border border-dashed border-[var(--color-border)]">
Este reporte no tiene parámetros definidos
</div>
)}
{previewUrl && (
<div className="mt-4 border border-[var(--color-border)]">
<iframe
src={previewUrl}
className="w-full h-[600px] border-none"
title="Vista previa del reporte"
/>
</div>
)}
{f4State && f4State.config && (
<ModalSearchTemplate
isOpen={!!f4State}
onClose={() => setF4State(null)}
title={f4State.param.label}
endpoint={f4State.config.endpoint}
fields={f4State.config.fields}
columns={f4State.config.columns}
baseFilter={f4State.config.baseFilter || ""}
seleccion={f4State.config.selectionMode || "unica"}
onConfirmarSeleccion={handleF4Confirm}
orderby={f4State.config.orderby}
rowKey={f4State.config.valueField}
/>
)}
</div>
);
}