912 lines
28 KiB
TypeScript
912 lines
28 KiB
TypeScript
/**
|
|
* Cliente API para comunicación con el backend
|
|
*/
|
|
import { getToken } from './auth';
|
|
import { browser } from '$app/environment';
|
|
import { toast } from 'svelte-sonner';
|
|
|
|
// Normalize API_BASE_URL to remove trailing slash
|
|
const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
|
|
|
|
export interface ApiResponse<T = any> {
|
|
data?: T;
|
|
error?: string;
|
|
validationErrors?: Array<{
|
|
field: string;
|
|
message: string;
|
|
code?: string;
|
|
solution?: string[];
|
|
value?: any;
|
|
}>;
|
|
status: number;
|
|
}
|
|
|
|
let isRefreshing = false;
|
|
let refreshSubscribers: ((token: string) => void)[] = [];
|
|
|
|
/**
|
|
* Agrega una petición a la cola de espera mientras se refresca el token
|
|
*/
|
|
function subscribeTokenRefresh(callback: (token: string) => void) {
|
|
refreshSubscribers.push(callback);
|
|
}
|
|
|
|
/**
|
|
* Notifica a todas las peticiones en espera que el token se ha refrescado
|
|
*/
|
|
function onTokenRefreshed(token: string) {
|
|
refreshSubscribers.forEach((callback) => callback(token));
|
|
refreshSubscribers = [];
|
|
}
|
|
|
|
/**
|
|
* Refresca el token silenciosamente usando el endpoint server-side.
|
|
*
|
|
* El servidor lee el refresh_token desde la cookie HttpOnly,
|
|
* llama a Keycloak, actualiza las cookies y devuelve el nuevo access_token.
|
|
* El refresh_token NUNCA es leído por este código JavaScript.
|
|
*/
|
|
async function refreshToken(): Promise<string | null> {
|
|
if (!browser) return null;
|
|
|
|
try {
|
|
const response = await fetch('/api-sveltekit/auth/silent-refresh', {
|
|
method: 'POST',
|
|
credentials: 'include', // Envía cookies HttpOnly automáticamente
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('❌ [API] Silent refresh falló, status:', response.status);
|
|
// Limpiar la cookie del access_token (no HttpOnly) para forzar re-login
|
|
document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
|
setTimeout(() => { window.location.href = '/login'; }, 1500);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json() as { access_token?: string };
|
|
|
|
if (data.access_token) {
|
|
// Actualizar cookie no-HttpOnly del access_token
|
|
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
document.cookie = `access_token=${data.access_token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax${secure}`;
|
|
|
|
// Actualizar authStore en memoria
|
|
try {
|
|
const { authStore } = await import('./auth');
|
|
authStore.setToken(data.access_token);
|
|
} catch {}
|
|
|
|
return data.access_token;
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('❌ [API] Error en silent refresh:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Realiza una petición al API con manejo automático de refresh token
|
|
*/
|
|
async function fetchApi<T = any>(
|
|
endpoint: string,
|
|
options: RequestInit = {},
|
|
retryCount = 0
|
|
): Promise<ApiResponse<T>> {
|
|
// Si ya estamos refrescando el token, esperar
|
|
if (isRefreshing && retryCount === 0) {
|
|
return new Promise((resolve) => {
|
|
subscribeTokenRefresh((newToken) => {
|
|
resolve(fetchApi<T>(endpoint, options, 1));
|
|
});
|
|
});
|
|
}
|
|
|
|
const token = getToken();
|
|
|
|
if (!token && !endpoint.includes('/auth/login')) {
|
|
console.warn('⚠️ [API] No hay token disponible para', endpoint);
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
...((options.headers as Record<string, string>) || {})
|
|
};
|
|
|
|
// Only set Content-Type to application/json if not already set and body is not FormData
|
|
if (!headers['Content-Type'] && !(options.body instanceof FormData)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
// Incluir tenant override para flujo SSO multi-tenant.
|
|
// sso_tenant_pub es una cookie no-HttpOnly que el servidor setea junto con sso_tenant_id.
|
|
if (browser) {
|
|
const tenantPub = document.cookie
|
|
.split('; ')
|
|
.find((c) => c.startsWith('sso_tenant_pub='))
|
|
?.split('=')[1];
|
|
if (tenantPub) {
|
|
headers['X-Tenant-Override'] = tenantPub;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include' // Importante: envía cookies con cada request
|
|
});
|
|
|
|
// Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token
|
|
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
|
|
// Si es 403 (Forbidden), mostrar toast de permisos insuficientes
|
|
if (response.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'
|
|
});
|
|
}
|
|
// Retornar el error 403 sin intentar refresh
|
|
const data = await response.json();
|
|
return {
|
|
error: data.detail || 'No tienes permisos para realizar esta acción',
|
|
status: 403
|
|
};
|
|
}
|
|
|
|
// Si es 401, intentar refrescar el token
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const newToken = await refreshToken();
|
|
|
|
if (newToken) {
|
|
// Token refrescado exitosamente
|
|
onTokenRefreshed(newToken);
|
|
isRefreshing = false;
|
|
// Reintentar la petición original con el nuevo token
|
|
return await fetchApi<T>(endpoint, options, 1);
|
|
} else {
|
|
console.error('❌ [API] No se pudo refrescar el token');
|
|
isRefreshing = false;
|
|
// Retornar error 401 para que la capa superior lo maneje
|
|
return {
|
|
error: 'Sesión expirada. Por favor, inicia sesión nuevamente.',
|
|
status: 401
|
|
};
|
|
}
|
|
} catch (refreshError) {
|
|
console.error('❌ [API] Error al refrescar:', refreshError);
|
|
isRefreshing = false;
|
|
return {
|
|
error: 'Error al refrescar la sesión',
|
|
status: 401
|
|
};
|
|
}
|
|
}
|
|
|
|
// Manejar respuestas sin contenido (204 No Content)
|
|
if (response.status === 204) {
|
|
return {
|
|
data: null as T,
|
|
status: response.status
|
|
};
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
// Manejo especial para errores 422 (validation error)
|
|
if (response.status === 422) {
|
|
// Errores de validación personalizados (con array errors)
|
|
if (data.errors && Array.isArray(data.errors)) {
|
|
return {
|
|
error: data.message || 'Error de validación',
|
|
validationErrors: data.errors,
|
|
status: response.status
|
|
};
|
|
}
|
|
// Errores de validación de FastAPI (con detail)
|
|
else if (data.detail) {
|
|
let errorMessage = 'Error de validación: ';
|
|
|
|
// FastAPI devuelve errores de validación en data.detail como array
|
|
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);
|
|
}
|
|
|
|
return {
|
|
error: errorMessage,
|
|
status: response.status
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
error: data.message || (typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail)) || 'Error en la petición',
|
|
status: response.status
|
|
};
|
|
}
|
|
|
|
return {
|
|
data,
|
|
status: response.status
|
|
};
|
|
} catch (error) {
|
|
console.error(`❌ [API] Error de conexión en ${endpoint}:`, error);
|
|
return {
|
|
error: 'Error de conexión con el servidor',
|
|
status: 0
|
|
};
|
|
}
|
|
}
|
|
|
|
/** 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> = {
|
|
...((options.headers as Record<string, string>) || {})
|
|
};
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Try to extract some useful message for debugging/UI.
|
|
const text = await response.text().catch(() => '');
|
|
throw new Error(text || `Error ${response.status} descargando archivo`);
|
|
}
|
|
return await response.blob();
|
|
}
|
|
|
|
// Métodos HTTP
|
|
export const api = {
|
|
get: <T = any>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
|
|
|
post: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
|
fetchApi<T>(endpoint, {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
...options
|
|
}),
|
|
|
|
put: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
|
fetchApi<T>(endpoint, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(body),
|
|
...options
|
|
}),
|
|
|
|
patch: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
|
fetchApi<T>(endpoint, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(body),
|
|
...options
|
|
}),
|
|
|
|
delete: <T = any>(endpoint: string, options: RequestInit = {}) => fetchApi<T>(endpoint, { method: 'DELETE', ...options }),
|
|
|
|
/**
|
|
* Download CSV template by template_id (generated from code, no static file).
|
|
* Returns blob and suggested filename for the browser download.
|
|
*/
|
|
async getCsvTemplateDownload(
|
|
templateId: string
|
|
): Promise<{ blob: Blob; filename: string }> {
|
|
const token = getToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
const response = await fetch(`${API_BASE_URL}/v1/a76/csv-templates/${templateId}`, {
|
|
method: 'GET',
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
if (!response.ok) {
|
|
const msg = response.status === 404 ? 'Plantilla no encontrada' : `Error ${response.status}`;
|
|
throw new Error(msg);
|
|
}
|
|
const blob = await response.blob();
|
|
let filename = `plantilla_${templateId}.csv`;
|
|
const disposition = response.headers.get('Content-Disposition');
|
|
if (disposition) {
|
|
const match = /filename="?([^";\n]+)"?/.exec(disposition);
|
|
if (match) filename = match[1].trim();
|
|
}
|
|
return { blob, filename };
|
|
},
|
|
|
|
// Endpoints específicos
|
|
auth: {
|
|
login: (credentials: { username: string; password: string; tenant_slug: string }) =>
|
|
api.post('/v1/auth/login/', credentials),
|
|
refresh: (refreshToken: string) =>
|
|
api.post('/v1/auth/refresh/', { refresh_token: refreshToken }),
|
|
logout: (data: { refresh_token: string, username?: string }) => api.post('/v1/auth/logout', data, { keepalive: true }),
|
|
me: () => api.get('/v1/auth/me/'),
|
|
health: () => api.get('/health'),
|
|
register: (data: {
|
|
username: string;
|
|
email: string;
|
|
password: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
tenant_slug: string;
|
|
invite_token?: string;
|
|
}) => api.post('/v1/auth/register', data),
|
|
},
|
|
|
|
tenants: {
|
|
list: (page = 1, pageSize = 50) =>
|
|
api.get(`/v1/tenants/?page=${page}&page_size=${pageSize}`),
|
|
get: (id: number) => api.get(`/v1/tenants/${id}/`),
|
|
create: (data: any) => api.post('/v1/tenants/', data),
|
|
update: (id: number, data: any) => api.put(`/v1/tenants/${id}/`, data)
|
|
},
|
|
|
|
licenses: {
|
|
get: (tenantId: number) => api.get(`/v1/licenses/tenant/${tenantId}/`),
|
|
myLicense: () => api.get('/v1/licenses/my-license/'),
|
|
usage: (tenantId: number) => api.get(`/v1/licenses/usage/${tenantId}/`),
|
|
validate: (tenantId: number) => api.get(`/v1/licenses/validate/${tenantId}/`)
|
|
},
|
|
|
|
// CSV import for Operaciones de Importación/Exportación (facturas: encabezados y partidas).
|
|
// Backend: api/v1/modules/a76/layouts_csv/facturas (rutas bajo /v1/a76/imports/).
|
|
imports: {
|
|
upload: (
|
|
file: File,
|
|
modelTarget: string,
|
|
footerConfig: any,
|
|
companyId: number,
|
|
operationType: string,
|
|
templateId?: string,
|
|
uploadOptions?: CsvFormDataUploadOptions
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (footerConfig) {
|
|
formData.append('footer_config', JSON.stringify(footerConfig));
|
|
}
|
|
if (templateId) {
|
|
formData.append('template_id', templateId);
|
|
}
|
|
|
|
const queryParams = new URLSearchParams({
|
|
company_id: String(companyId),
|
|
operation_type: operationType || 'imp'
|
|
}).toString();
|
|
|
|
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) =>
|
|
api.post(`/v1/a76/imports/${jobId}/commit`, { model_target: modelTarget }),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Operaciones de Exportación (encabezado y partidas).
|
|
// Backend: layouts_csv/exportacion — rutas /v1/a76/imports/exportacion/
|
|
exportacionImports: {
|
|
upload: (
|
|
file: File,
|
|
modelTarget: string,
|
|
footerConfig: any,
|
|
companyId: number,
|
|
templateId?: string,
|
|
uploadOptions?: CsvFormDataUploadOptions
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (footerConfig) {
|
|
formData.append('footer_config', JSON.stringify(footerConfig));
|
|
}
|
|
if (templateId) {
|
|
formData.append('template_id', templateId);
|
|
}
|
|
|
|
const queryParams = new URLSearchParams({
|
|
company_id: String(companyId),
|
|
operation_type: 'exp'
|
|
}).toString();
|
|
|
|
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) =>
|
|
api.post(`/v1/a76/imports/exportacion/${jobId}/commit`, { model_target: modelTarget }),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/imports/exportacion/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Agentes Aduanales (flujo propio en customs_brokers/imports)
|
|
customsBrokerImports: {
|
|
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/customs-brokers/imports/upload?company_id=${companyId}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/customs-brokers/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/customs-brokers/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/customs-brokers/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Clientes y Proveedores (flujo propio en clients_and_providers/imports)
|
|
clientProviderImports: {
|
|
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/clients-providers/imports/upload?company_id=${companyId}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/clients-providers/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/clients-providers/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/clients-providers/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Tipos de Cambio (flujo en general_catalogs/exchange_rate/imports)
|
|
exchangeRateImports: {
|
|
upload: (
|
|
file: File,
|
|
companyId: number,
|
|
params?: { reemplazar_sin_preguntar?: boolean; date_format?: string },
|
|
uploadOptions?: CsvFormDataUploadOptions
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const search = new URLSearchParams({ company_id: String(companyId) });
|
|
if (params?.reemplazar_sin_preguntar !== undefined)
|
|
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 fetchApiFormDataPost(
|
|
`/v1/a76/exchange-rate/imports/upload?${search.toString()}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/exchange-rate/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/exchange-rate/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/exchange-rate/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Fracción Americana (us_tariff_fractions/imports)
|
|
americanFractionImports: {
|
|
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/us-tariff-fractions/imports/upload?company_id=${companyId}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/us-tariff-fractions/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/us-tariff-fractions/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/us-tariff-fractions/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Pedimentos (pedimentos/imports)
|
|
pedimentosImports: {
|
|
upload: (
|
|
file: File,
|
|
companyId: number,
|
|
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 fetchApiFormDataPost(
|
|
`/v1/a76/pedimentos/imports/upload?${search.toString()}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/pedimentos/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/pedimentos/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/pedimentos/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Clases de Materiales (classes/imports)
|
|
materialClassImports: {
|
|
upload: (
|
|
file: File,
|
|
companyId: number,
|
|
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 fetchApiFormDataPost(
|
|
`/v1/a76/classes/imports/upload?${search.toString()}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/classes/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/classes/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/classes/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Vehículos / Transportes (transportation/vehicles/imports)
|
|
vehicleImports: {
|
|
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));
|
|
const uploadOpts =
|
|
options?.onUploadProgress != null ? { onUploadProgress: options.onUploadProgress } : undefined;
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/transportation/vehicles/imports/upload?${params.toString()}`,
|
|
formData,
|
|
uploadOpts
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/transportation/vehicles/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/transportation/vehicles/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/transportation/vehicles/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Conductores (drivers/imports)
|
|
driverImports: {
|
|
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/drivers/imports/upload?company_id=${companyId}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/drivers/imports/${jobId}/status`),
|
|
commit: (jobId: string) => api.post(`/v1/a76/drivers/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/drivers/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Trailers y Cajas (transportation/trailers/imports)
|
|
trailerImports: {
|
|
upload: (
|
|
file: File,
|
|
companyId: number,
|
|
params?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const actualizar = params?.actualizar ?? false;
|
|
const uploadOpts =
|
|
params?.onUploadProgress != null ? { onUploadProgress: params.onUploadProgress } : undefined;
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/transportation/trailers/imports/upload?company_id=${companyId}&actualizar=${actualizar}`,
|
|
formData,
|
|
uploadOpts
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/transportation/trailers/imports/${jobId}/status`),
|
|
commit: (jobId: string) =>
|
|
api.post(`/v1/a76/transportation/trailers/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/transportation/trailers/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Transportistas (transporters/imports)
|
|
transporterImports: {
|
|
upload: (
|
|
file: File,
|
|
companyId: number,
|
|
params?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
|
|
) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const actualizar = params?.actualizar ?? false;
|
|
const uploadOpts =
|
|
params?.onUploadProgress != null ? { onUploadProgress: params.onUploadProgress } : undefined;
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/transporters/imports/upload?company_id=${companyId}&actualizar=${actualizar}`,
|
|
formData,
|
|
uploadOpts
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/transporters/imports/${jobId}/status`),
|
|
commit: (jobId: string) => api.post(`/v1/a76/transporters/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/transporters/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for Números de parte (parts/imports)
|
|
partNumberImports: {
|
|
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));
|
|
const uploadOpts =
|
|
options?.onUploadProgress != null ? { onUploadProgress: options.onUploadProgress } : undefined;
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/parts/imports/upload?${params.toString()}`,
|
|
formData,
|
|
uploadOpts
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/parts/imports/${jobId}/status`),
|
|
commit: (jobId: string) => api.post(`/v1/a76/parts/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) =>
|
|
fetchBlob(`/v1/a76/parts/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// CSV import for BOMs (boms/imports)
|
|
bomImports: {
|
|
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return fetchApiFormDataPost(
|
|
`/v1/a76/boms/imports/upload?company_id=${companyId}`,
|
|
formData,
|
|
uploadOptions
|
|
);
|
|
},
|
|
status: (jobId: string) => api.get(`/v1/a76/boms/imports/${jobId}/status`),
|
|
commit: (jobId: string) => api.post(`/v1/a76/boms/imports/${jobId}/commit`, {}),
|
|
downloadScanErrorsCsv: (jobId: string) => fetchBlob(`/v1/a76/boms/imports/${jobId}/errors/scan-csv`)
|
|
},
|
|
|
|
// Generic request for custom needs (like file uploads)
|
|
request: <T = any>(endpoint: string, options: RequestInit = {}) => fetchApi<T>(endpoint, options)
|
|
};
|