panels-origin/web-panel/composables/api-response.ts
Cursor Agent fa3656d142
fix: mensajes claros al subir presupuesto que no es Excel
El toast de error salía vacío porque el preview 400 no traía message.
Ahora solo se aceptan .xlsx/.xls, y si el Excel no tiene el formato
de presupuesto se explica qué columnas o plantilla usar.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:34:57 +00:00

108 lines
3.2 KiB
TypeScript

export type FrontRpcCode = "VALIDATION" | "NETWORK" | "INTERNAL";
export type FrontResponse<T = unknown> = {
ok: boolean;
code: FrontRpcCode | string;
layer: "front";
message: string;
context?: Record<string, unknown>;
data?: T | null;
errors?: Record<string, string> | string[] | null;
};
export function buildFrontError(
message: string,
code: FrontRpcCode = "VALIDATION",
context?: Record<string, unknown>,
errors?: Record<string, string> | string[] | null,
): FrontResponse {
return {
ok: false,
code,
layer: "front",
message,
context,
data: null,
errors: errors ?? null,
};
}
export function buildFrontNetworkError(method: string, path: string): FrontResponse {
return buildFrontError(
`No se pudo conectar con el servidor en ${method} ${path} — verifique su conexión`,
"NETWORK",
{ route: `${method} ${path}` },
);
}
export function isApiEnvelope(value: unknown): value is {
ok: boolean;
code: string;
status?: number;
layer: string;
message: string;
data?: unknown;
} {
return !!value && typeof value === "object"
&& "ok" in value && "message" in value && "layer" in value;
}
/** Normaliza respuestas RPC `{ ok, data, message }` al shape plano que esperan las pantallas. */
export function unwrapApiPayload<T>(body: unknown): T {
if (!isApiEnvelope(body)) return body as T;
const env = body;
const data = env.data;
const spread = data && typeof data === "object" && !Array.isArray(data)
? data as Record<string, unknown>
: {};
return {
...spread,
data,
ok: env.ok,
code: env.code,
message: env.message,
status: env.status,
layer: env.layer,
} as T;
}
function firstListedError(value: object): string | null {
if (!("errors" in value) || value.errors == null) return null;
const errors = (value as { errors: unknown }).errors;
if (typeof errors === "string" && errors.trim()) return errors;
if (!Array.isArray(errors)) return null;
for (const item of errors) {
if (typeof item === "string" && item.trim()) return item;
if (item && typeof item === "object" && "messages" in item) {
const msgs = (item as { messages: unknown }).messages;
if (Array.isArray(msgs)) {
const first = msgs.find((msg) => typeof msg === "string" && msg.trim());
if (typeof first === "string") return first;
}
}
}
return null;
}
/** Muestra el mensaje detallado de la API o front sin reescribirlo. */
export function displayErrorMessage(value: unknown, fallback = "Ocurrió un error"): string {
if (isApiEnvelope(value)) {
const listed = firstListedError(value);
const message = value.message?.trim();
if (message) return message;
if (listed) return listed;
}
if (value && typeof value === "object") {
if ("message" in value) {
const message = String((value as { message: unknown }).message || "").trim();
if (message) return message;
}
if ("error" in value) {
const error = String((value as { error: unknown }).error || "").trim();
if (error) return error;
}
const listed = firstListedError(value);
if (listed) return listed;
}
return fallback || "Ocurrió un error";
}