feature/table-celery-tasks-resume

This commit is contained in:
2026-03-24 16:17:13 -06:00
parent 481b295ab0
commit 65eec3c4d3
9 changed files with 1418 additions and 132 deletions

View File

@@ -243,6 +243,193 @@ async function fetchApi<T = any>(
}
}
/** Opciones para subidas CSV (FormData) con progreso de red. */
export type CsvFormDataUploadOptions = {
onUploadProgress?: (e: { loaded: number; total: number }) => void;
};
/**
* POST multipart/form-data con XMLHttpRequest para exponer progreso de subida.
* Misma semántica de auth/401/403/422 que fetchApi.
*/
async function fetchApiFormDataPost<T = any>(
endpoint: string,
formData: FormData,
opts: CsvFormDataUploadOptions & { retryCount?: number } = {}
): Promise<ApiResponse<T>> {
const retryCount = opts.retryCount ?? 0;
if (isRefreshing && retryCount === 0) {
return new Promise((resolve) => {
subscribeTokenRefresh(() => {
resolve(fetchApiFormDataPost<T>(endpoint, formData, { ...opts, retryCount: 1 }));
});
});
}
return new Promise((resolve) => {
const token = getToken();
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE_URL}${endpoint}`);
xhr.withCredentials = true;
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
xhr.upload.onprogress = (ev) => {
if (!opts.onUploadProgress) return;
if (ev.lengthComputable) {
opts.onUploadProgress({ loaded: ev.loaded, total: ev.total });
} else {
opts.onUploadProgress({ loaded: ev.loaded, total: 0 });
}
};
xhr.onload = () => {
void (async () => {
const status = xhr.status;
let data: any = null;
if (xhr.responseText) {
try {
data = JSON.parse(xhr.responseText) as any;
} catch {
data = null;
}
}
if ((status === 401 || status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
if (status === 403) {
if (browser) {
toast.error('No tienes permisos para realizar esta acción', {
duration: 4000,
description: 'Contacta a tu administrador si crees que esto es un error'
});
}
resolve({
error: data?.detail || 'No tienes permisos para realizar esta acción',
status: 403
});
return;
}
isRefreshing = true;
try {
const newToken = await refreshToken();
if (newToken) {
onTokenRefreshed(newToken);
isRefreshing = false;
resolve(await fetchApiFormDataPost<T>(endpoint, formData, { ...opts, retryCount: 1 }));
} else {
console.error('❌ [API] No se pudo refrescar el token');
isRefreshing = false;
resolve({
error: 'Sesión expirada. Por favor, inicia sesión nuevamente.',
status: 401
});
}
} catch (refreshError) {
console.error('❌ [API] Error al refrescar:', refreshError);
isRefreshing = false;
resolve({
error: 'Error al refrescar la sesión',
status: 401
});
}
return;
}
if (status === 204) {
resolve({
data: null as T,
status
});
return;
}
if (status === 0) {
resolve({
error: 'Error de conexión con el servidor',
status: 0
});
return;
}
if (status < 200 || status >= 300) {
if (status === 422 && data) {
if (data.errors && Array.isArray(data.errors)) {
resolve({
error: data.message || 'Error de validación',
validationErrors: data.errors,
status: 422
});
return;
}
if (data.detail) {
let errorMessage = 'Error de validación: ';
if (Array.isArray(data.detail)) {
const errors = data.detail
.map((err: any) => {
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
return `${field}: ${err.msg}`;
})
.join(', ');
errorMessage += errors;
} else if (typeof data.detail === 'string') {
errorMessage = data.detail;
} else {
errorMessage += JSON.stringify(data.detail);
}
resolve({
error: errorMessage,
status: 422
});
return;
}
}
resolve({
error:
data?.message ||
(typeof data?.detail === 'string' ? data.detail : JSON.stringify(data?.detail)) ||
'Error en la petición',
status
});
return;
}
if (data === null && xhr.responseText) {
resolve({
error: 'Respuesta inválida del servidor',
status
});
return;
}
resolve({
data,
status
});
})();
};
xhr.onerror = () => {
resolve({
error: 'Error de conexión con el servidor',
status: 0
});
};
try {
xhr.send(formData);
} catch (error) {
console.error(`❌ [API] Error al enviar ${endpoint}:`, error);
resolve({
error: 'Error de conexión con el servidor',
status: 0
});
}
});
}
async function fetchBlob(endpoint: string, options: RequestInit = {}): Promise<Blob> {
const token = getToken();
const headers: Record<string, string> = {
@@ -357,7 +544,8 @@ export const api = {
footerConfig: any,
companyId: number,
operationType: string,
templateId?: string
templateId?: string,
uploadOptions?: CsvFormDataUploadOptions
) => {
const formData = new FormData();
formData.append('file', file);
@@ -373,10 +561,11 @@ export const api = {
operation_type: operationType || 'imp'
}).toString();
return fetchApi(`/v1/a76/imports/upload/${modelTarget}?${queryParams}`, {
method: 'POST',
body: formData
});
return fetchApiFormDataPost(
`/v1/a76/imports/upload/${modelTarget}?${queryParams}`,
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/imports/${jobId}/status`),
commit: (jobId: string, modelTarget: string) =>
@@ -393,7 +582,8 @@ export const api = {
modelTarget: string,
footerConfig: any,
companyId: number,
templateId?: string
templateId?: string,
uploadOptions?: CsvFormDataUploadOptions
) => {
const formData = new FormData();
formData.append('file', file);
@@ -409,10 +599,11 @@ export const api = {
operation_type: 'exp'
}).toString();
return fetchApi(`/v1/a76/imports/exportacion/upload/${modelTarget}?${queryParams}`, {
method: 'POST',
body: formData
});
return fetchApiFormDataPost(
`/v1/a76/imports/exportacion/upload/${modelTarget}?${queryParams}`,
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/imports/exportacion/${jobId}/status`),
commit: (jobId: string, modelTarget: string) =>
@@ -423,12 +614,13 @@ export const api = {
// CSV import for Agentes Aduanales (flujo propio en customs_brokers/imports)
customsBrokerImports: {
upload: (file: File, companyId: number) => {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
const formData = new FormData();
formData.append('file', file);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/customs-brokers/imports/upload?company_id=${companyId}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/customs-brokers/imports/${jobId}/status`),
@@ -440,12 +632,13 @@ export const api = {
// CSV import for Clientes y Proveedores (flujo propio en clients_and_providers/imports)
clientProviderImports: {
upload: (file: File, companyId: number) => {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
const formData = new FormData();
formData.append('file', file);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/clients-providers/imports/upload?company_id=${companyId}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/clients-providers/imports/${jobId}/status`),
@@ -460,7 +653,8 @@ export const api = {
upload: (
file: File,
companyId: number,
params?: { reemplazar_sin_preguntar?: boolean; date_format?: string }
params?: { reemplazar_sin_preguntar?: boolean; date_format?: string },
uploadOptions?: CsvFormDataUploadOptions
) => {
const formData = new FormData();
formData.append('file', file);
@@ -469,9 +663,10 @@ export const api = {
search.set('reemplazar_sin_preguntar', String(!!params.reemplazar_sin_preguntar));
if (params?.date_format != null && params.date_format !== '')
search.set('date_format', params.date_format);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/exchange-rate/imports/upload?${search.toString()}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/exchange-rate/imports/${jobId}/status`),
@@ -483,12 +678,13 @@ export const api = {
// CSV import for Fracción Americana (us_tariff_fractions/imports)
americanFractionImports: {
upload: (file: File, companyId: number) => {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
const formData = new FormData();
formData.append('file', file);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/us-tariff-fractions/imports/upload?company_id=${companyId}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/us-tariff-fractions/imports/${jobId}/status`),
@@ -503,16 +699,18 @@ export const api = {
upload: (
file: File,
companyId: number,
params?: { actualizar?: boolean; dateFormat?: string }
params?: { actualizar?: boolean; dateFormat?: string },
uploadOptions?: CsvFormDataUploadOptions
) => {
const formData = new FormData();
formData.append('file', file);
const search = new URLSearchParams({ company_id: String(companyId) });
if (params?.actualizar !== undefined) search.set('actualizar', String(!!params.actualizar));
if (params?.dateFormat != null) search.set('dateFormat', params.dateFormat);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/pedimentos/imports/upload?${search.toString()}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/pedimentos/imports/${jobId}/status`),
@@ -527,16 +725,18 @@ export const api = {
upload: (
file: File,
companyId: number,
params?: { actualizar?: boolean; siempre_toda?: boolean }
params?: { actualizar?: boolean; siempre_toda?: boolean },
uploadOptions?: CsvFormDataUploadOptions
) => {
const formData = new FormData();
formData.append('file', file);
const search = new URLSearchParams({ company_id: String(companyId) });
if (params?.actualizar !== undefined) search.set('actualizar', String(!!params.actualizar));
if (params?.siempre_toda !== undefined) search.set('siempre_toda', String(!!params.siempre_toda));
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/classes/imports/upload?${search.toString()}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/classes/imports/${jobId}/status`),
@@ -548,14 +748,21 @@ export const api = {
// CSV import for Vehículos / Transportes (transportation/vehicles/imports)
vehicleImports: {
upload: (file: File, companyId: number, options?: { actualizar?: boolean }) => {
upload: (
file: File,
companyId: number,
options?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
) => {
const formData = new FormData();
formData.append('file', file);
const params = new URLSearchParams({ company_id: String(companyId) });
if (options?.actualizar !== undefined) params.set('actualizar', String(options.actualizar));
return fetchApi(
const uploadOpts =
options?.onUploadProgress != null ? { onUploadProgress: options.onUploadProgress } : undefined;
return fetchApiFormDataPost(
`/v1/a76/transportation/vehicles/imports/upload?${params.toString()}`,
{ method: 'POST', body: formData }
formData,
uploadOpts
);
},
status: (jobId: string) => api.get(`/v1/a76/transportation/vehicles/imports/${jobId}/status`),
@@ -567,12 +774,13 @@ export const api = {
// CSV import for Conductores (drivers/imports)
driverImports: {
upload: (file: File, companyId: number) => {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
const formData = new FormData();
formData.append('file', file);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/drivers/imports/upload?company_id=${companyId}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/drivers/imports/${jobId}/status`),
@@ -583,13 +791,20 @@ export const api = {
// CSV import for Trailers y Cajas (transportation/trailers/imports)
trailerImports: {
upload: (file: File, companyId: number, params?: { actualizar?: boolean }) => {
upload: (
file: File,
companyId: number,
params?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
) => {
const formData = new FormData();
formData.append('file', file);
const actualizar = params?.actualizar ?? false;
return fetchApi(
const uploadOpts =
params?.onUploadProgress != null ? { onUploadProgress: params.onUploadProgress } : undefined;
return fetchApiFormDataPost(
`/v1/a76/transportation/trailers/imports/upload?company_id=${companyId}&actualizar=${actualizar}`,
{ method: 'POST', body: formData }
formData,
uploadOpts
);
},
status: (jobId: string) => api.get(`/v1/a76/transportation/trailers/imports/${jobId}/status`),
@@ -601,13 +816,20 @@ export const api = {
// CSV import for Transportistas (transporters/imports)
transporterImports: {
upload: (file: File, companyId: number, params?: { actualizar?: boolean }) => {
upload: (
file: File,
companyId: number,
params?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
) => {
const formData = new FormData();
formData.append('file', file);
const actualizar = params?.actualizar ?? false;
return fetchApi(
const uploadOpts =
params?.onUploadProgress != null ? { onUploadProgress: params.onUploadProgress } : undefined;
return fetchApiFormDataPost(
`/v1/a76/transporters/imports/upload?company_id=${companyId}&actualizar=${actualizar}`,
{ method: 'POST', body: formData }
formData,
uploadOpts
);
},
status: (jobId: string) => api.get(`/v1/a76/transporters/imports/${jobId}/status`),
@@ -618,15 +840,27 @@ export const api = {
// CSV import for Números de parte (parts/imports)
partNumberImports: {
upload: (file: File, companyId: number, options?: { actualizar?: boolean; reemplazar_sin_preguntar?: boolean }) => {
upload: (
file: File,
companyId: number,
options?: {
actualizar?: boolean;
reemplazar_sin_preguntar?: boolean;
onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'];
}
) => {
const formData = new FormData();
formData.append('file', file);
const params = new URLSearchParams({ company_id: String(companyId) });
if (options?.actualizar !== undefined) params.set('actualizar', String(options.actualizar));
if (options?.reemplazar_sin_preguntar !== undefined) params.set('reemplazar_sin_preguntar', String(options.reemplazar_sin_preguntar));
return fetchApi(
if (options?.reemplazar_sin_preguntar !== undefined)
params.set('reemplazar_sin_preguntar', String(options.reemplazar_sin_preguntar));
const uploadOpts =
options?.onUploadProgress != null ? { onUploadProgress: options.onUploadProgress } : undefined;
return fetchApiFormDataPost(
`/v1/a76/parts/imports/upload?${params.toString()}`,
{ method: 'POST', body: formData }
formData,
uploadOpts
);
},
status: (jobId: string) => api.get(`/v1/a76/parts/imports/${jobId}/status`),
@@ -637,12 +871,13 @@ export const api = {
// CSV import for BOMs (boms/imports)
bomImports: {
upload: (file: File, companyId: number) => {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
const formData = new FormData();
formData.append('file', file);
return fetchApi(
return fetchApiFormDataPost(
`/v1/a76/boms/imports/upload?company_id=${companyId}`,
{ method: 'POST', body: formData }
formData,
uploadOptions
);
},
status: (jobId: string) => api.get(`/v1/a76/boms/imports/${jobId}/status`),