- 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)
272 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
}
|