feature/errores-legibles-tabla
This commit is contained in:
@@ -419,3 +419,109 @@ export function formatItemError(error: any): string {
|
||||
// 5. Default messages
|
||||
return detail || data?.message || error.message || 'Error al procesar la solicitud.';
|
||||
}
|
||||
|
||||
export type ItemErrorPanelRow = { field: string; message: string; fieldPath?: string };
|
||||
|
||||
const INVOICE_ITEM_FIELD_LABEL_SEP = ' - ';
|
||||
|
||||
/**
|
||||
* For the partidas error panel "Campo" column: hide the standard "Partida N - …" prefix.
|
||||
* Only affects display via {@link formatItemErrorRows}.
|
||||
*/
|
||||
function shortenItemErrorFieldLabel(field: string): string {
|
||||
const f = (field || '').trim();
|
||||
if (!f) return f;
|
||||
const i = f.indexOf(INVOICE_ITEM_FIELD_LABEL_SEP);
|
||||
if (i === -1) return f;
|
||||
const rest = f.slice(i + INVOICE_ITEM_FIELD_LABEL_SEP.length).trim();
|
||||
return rest || f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the multiline string returned by {@link formatItemError} into structured rows
|
||||
* for UI tables (field label + user-facing message).
|
||||
*/
|
||||
export function parseFormattedItemErrorText(text: string): ItemErrorPanelRow[] {
|
||||
const raw = (text || '').trim();
|
||||
if (!raw) return [];
|
||||
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const rows: ItemErrorPanelRow[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^por favor revisa/i.test(line)) continue;
|
||||
if (/^please review/i.test(line)) continue;
|
||||
|
||||
const withoutBullet = line.replace(/^•\s*/, '').trim();
|
||||
const sep = ': ';
|
||||
const idx = withoutBullet.indexOf(sep);
|
||||
|
||||
if (idx === -1) {
|
||||
rows.push({ field: '', message: withoutBullet });
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
field: withoutBullet.slice(0, idx).trim(),
|
||||
message: withoutBullet.slice(idx + sep.length).trim()
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0 && raw) {
|
||||
rows.push({ field: '', message: raw });
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Structured rows for error panels, preserving the technical `fieldPath` when available. */
|
||||
export function formatItemErrorRows(error: any): ItemErrorPanelRow[] {
|
||||
if (!error) return [];
|
||||
|
||||
const isApiResponse = 'status' in error && ('error' in error || 'validationErrors' in error);
|
||||
const status = isApiResponse ? error.status : error?.response?.status;
|
||||
const validationErrors: any[] | null = isApiResponse ? error.validationErrors : null;
|
||||
const legacyDetailArray: any[] | null =
|
||||
!isApiResponse && Array.isArray(error?.response?.data?.detail)
|
||||
? error.response.data.detail
|
||||
: null;
|
||||
|
||||
if (status === 422 && Array.isArray(validationErrors) && validationErrors.length > 0) {
|
||||
return validationErrors.map((err: any) => {
|
||||
const rawPath: string = err.field || '';
|
||||
const lineMatch = rawPath.match(/^line\[(\d+)\]\.(.+)$/i);
|
||||
const suffix = (lineMatch?.[2] ?? rawPath).replace(/^body\./i, '');
|
||||
const fieldLabel = FIELD_MAP[suffix] ?? suffix.replace(/\./g, ' → ');
|
||||
const humanField = lineMatch ? `Partida ${lineMatch[1]} - ${fieldLabel}` : fieldLabel;
|
||||
const msg = formatFriendlyFieldMessage(humanField, err.message || 'error de validación', err.code, suffix);
|
||||
return {
|
||||
field: shortenItemErrorFieldLabel(humanField),
|
||||
message: msg,
|
||||
fieldPath: suffix || undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (status === 422 && Array.isArray(legacyDetailArray) && legacyDetailArray.length > 0) {
|
||||
return legacyDetailArray.map((err: any) => {
|
||||
const locPath: string = (err.loc as string[] || []).filter((l) => l !== 'body').join('.');
|
||||
const suffix = locPath.replace(/^line\[\d+\]\./i, '').replace(/^body\./i, '');
|
||||
const fieldName = humanizeFieldPath(locPath);
|
||||
const msg = formatFriendlyFieldMessage(fieldName, err.msg || 'error de validación', err.type, suffix);
|
||||
return {
|
||||
field: shortenItemErrorFieldLabel(fieldName),
|
||||
message: msg,
|
||||
fieldPath: suffix || undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return parseFormattedItemErrorText(formatItemError(error)).map((r) => ({
|
||||
field: shortenItemErrorFieldLabel(r.field),
|
||||
message: r.message
|
||||
}));
|
||||
}
|
||||
|
||||
17
frontend/src/lib/utils/partida-notice-interact-outside.ts
Normal file
17
frontend/src/lib/utils/partida-notice-interact-outside.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const PARTIDA_NOTICE_LAYER_SELECTOR = '[data-partida-notice-layer]';
|
||||
|
||||
/**
|
||||
* Composes bits-ui `onInteractOutside` so clicks on the portaled invoice-items
|
||||
* {@link ErrorPanelNotice} are not treated as “outside” the Sheet/Dialog.
|
||||
*/
|
||||
export function withPartidaNoticeInteractOutside(
|
||||
userOnInteractOutside?: (e: PointerEvent) => void
|
||||
): (e: PointerEvent) => void {
|
||||
return (e: PointerEvent) => {
|
||||
const t = e.target;
|
||||
if (t instanceof Element && t.closest(PARTIDA_NOTICE_LAYER_SELECTOR)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
userOnInteractOutside?.(e);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user