feature/table-celery-tasks-resume
This commit is contained in:
@@ -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`),
|
||||
|
||||
Reference in New Issue
Block a user