diff --git a/src/App.jsx b/src/App.jsx index a6b7de3..ebfcd7f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,8 @@ import Expedientes from './pages/Expedientes'; import Organization from './pages/Organization'; import Users from './pages/Users'; import UserForm from './pages/UserForm'; +import Profiles from './pages/Profiles'; +import ProfileForm from './pages/ProfileForm'; import Reports from './pages/Reports'; import Settings from './pages/Settings'; import Importers from './pages/Importers'; @@ -87,6 +89,21 @@ function AppContent() { } /> + + + + } /> + + + + } /> + + + + } /> diff --git a/src/api/apiError.js b/src/api/apiError.js new file mode 100644 index 0000000..d9b3a9c --- /dev/null +++ b/src/api/apiError.js @@ -0,0 +1,19 @@ +/** + * Extrae el mensaje de error de una respuesta HTTP fallida. + * Lee el JSON del body y devuelve `detail`, `message` o `error` del backend. + * Si no hay JSON, devuelve el texto o un fallback genérico con el status. + */ +export async function extractApiError(response) { + const status = response.status; + try { + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + const body = await response.json(); + return body.detail || body.message || body.error || `Error ${status}`; + } + const text = await response.text(); + return text || `Error ${status}`; + } catch { + return `Error ${status}`; + } +} diff --git a/src/api/coves.js b/src/api/coves.js index 18f30ab..595bda4 100644 --- a/src/api/coves.js +++ b/src/api/coves.js @@ -1,95 +1,56 @@ import { fetchWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; const API_BASE_URL = import.meta.env.VITE_EFC_API_URL; export const fetchPedimentoCoves = async (pedimentoId, page = 1, pageSize = 10, filters = {}) => { - try { - const params = new URLSearchParams({ - pedimento: pedimentoId, - page: page.toString(), - page_size: pageSize.toString(), - }); + const params = new URLSearchParams({ + pedimento: pedimentoId, + page: page.toString(), + page_size: pageSize.toString(), + }); - // Agregar filtros si existen - if (filters.numero_cove) { - params.append('numero_cove__icontains', filters.numero_cove); - } - if (filters.cove_descargado !== undefined && filters.cove_descargado !== '') { - params.append('cove_descargado', filters.cove_descargado); - } - if (filters.acuse_cove_descargado !== undefined && filters.acuse_cove_descargado !== '') { - params.append('acuse_cove_descargado', filters.acuse_cove_descargado); - } - if (filters.date_from) { - params.append('created_at__gte', filters.date_from); - } - if (filters.date_to) { - params.append('created_at__lte', filters.date_to); - } + if (filters.numero_cove) params.append('numero_cove__icontains', filters.numero_cove); + if (filters.cove_descargado !== undefined && filters.cove_descargado !== '') params.append('cove_descargado', filters.cove_descargado); + if (filters.acuse_cove_descargado !== undefined && filters.acuse_cove_descargado !== '') params.append('acuse_cove_descargado', filters.acuse_cove_descargado); + if (filters.date_from) params.append('created_at__gte', filters.date_from); + if (filters.date_to) params.append('created_at__lte', filters.date_to); - const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/?${params}`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/?${params}`); + if (!response.ok) throw new Error(await extractApiError(response)); - const data = await response.json(); - return { - results: data.results, - count: data.count, - next: data.next, - previous: data.previous - }; - } catch (error) { - console.error('Error fetching COVEs:', error); - throw error; - } + const data = await response.json(); + return { results: data.results, count: data.count, next: data.next, previous: data.previous }; }; export const downloadCove = async (coveId) => { - try { - const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/${coveId}/download/`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/${coveId}/download/`); + if (!response.ok) throw new Error(await extractApiError(response)); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `COVE_${coveId}.pdf`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Error downloading COVE:', error); - throw error; - } + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `COVE_${coveId}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); }; export const downloadAcuseCove = async (coveId) => { - try { - const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/${coveId}/download-acuse/`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/${coveId}/download-acuse/`); + if (!response.ok) throw new Error(await extractApiError(response)); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `ACUSE_COVE_${coveId}.pdf`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Error downloading COVE acuse:', error); - throw error; - } -}; \ No newline at end of file + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `ACUSE_COVE_${coveId}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); +}; diff --git a/src/api/datastage.js b/src/api/datastage.js index 7670a8e..852d72e 100644 --- a/src/api/datastage.js +++ b/src/api/datastage.js @@ -1,44 +1,42 @@ -// Consultar el estado de un task por task_id +import { getWithAuth, postWithAuth, patchWithAuth, deleteWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; + +const API_BASE = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`; + export async function fetchTaskStatus(task_id) { const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/task-status/?task_id=${encodeURIComponent(task_id)}`; const res = await getWithAuth(url); - if (!res.ok) throw new Error('Error al consultar el estado del task'); + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } -import.meta.env; -import { getWithAuth, postWithAuth, patchWithAuth, deleteWithAuth } from '../fetchWithAuth'; -const API_BASE = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`; - export async function fetchDatastages(page = 1, page_size = 10) { const url = `${API_BASE}?page=${page}&page_size=${page_size}`; const res = await getWithAuth(url); - if (!res.ok) throw new Error('Error al obtener datastages'); - const data = await res.json(); - // Si la respuesta es paginada, devolver el objeto completo - return data; + if (!res.ok) throw new Error(await extractApiError(res)); + return res.json(); } export async function fetchDatastageDetail(id) { const res = await getWithAuth(`${API_BASE}${id}/`); - if (!res.ok) throw new Error('Error al obtener detalle'); + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } export async function createDatastage(data) { const res = await postWithAuth(API_BASE, data); - if (!res.ok) throw new Error('Error al crear datastage'); + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } export async function updateDatastage(id, data) { const res = await patchWithAuth(`${API_BASE}${id}/`, data); - if (!res.ok) throw new Error('Error al actualizar datastage'); + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } export async function deleteDatastage(id) { const res = await deleteWithAuth(`${API_BASE}${id}/`); - if (!res.ok) throw new Error('Error al eliminar datastage'); + if (!res.ok) throw new Error(await extractApiError(res)); return true; } diff --git a/src/api/documentos.ts b/src/api/documentos.ts index 19c9496..330e8c2 100644 --- a/src/api/documentos.ts +++ b/src/api/documentos.ts @@ -1,5 +1,6 @@ // src/api/pedimentoDocuments.ts import { fetchWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; export interface PedimentoDocument { id: string; @@ -47,6 +48,6 @@ export async function fetchPedimentoDocuments( `${API_URL}/record/documents/?${params.toString()}` ); - if (!res.ok) throw new Error('No autorizado o error en la petición'); + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } diff --git a/src/api/documents.js b/src/api/documents.js index d43c6e1..484b545 100644 --- a/src/api/documents.js +++ b/src/api/documents.js @@ -1,74 +1,43 @@ - -/** - * @typedef {Object} Document - * @property {string} id - * @property {string} organizacion - * @property {string} pedimento - * @property {string} archivo - * @property {number} document_type - * @property {number} size - * @property {string} extension - * @property {string} created_at - * @property {string} updated_at - */ - -/** - * @typedef {Object} DocumentsResponse - * @property {number} count - * @property {string|null} next - * @property {string|null} previous - * @property {Document[]} results - */ - import { refreshToken } from './auth'; +import { extractApiError } from './apiError'; const API_URL = import.meta.env.VITE_EFC_API_URL; -/** - * Obtiene la lista de documentos (pedimentos) - * @param {string} token - * @returns {Promise} - */ + export async function fetchDocuments(token, queryString = '') { let url = `${API_URL}/customs/pedimentos/`; - if (queryString) { - url += `?${queryString}`; - } + if (queryString) url += `?${queryString}`; + let res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); + if (res.status === 401) { - // Intentar refrescar el token const refresh = localStorage.getItem('refresh'); if (refresh) { try { const data = await refreshToken(refresh); localStorage.setItem('access', data.access); - // Reintenta la petición con el nuevo access token res = await fetch(`${API_URL}/customs/pedimentos/`, { headers: { 'Authorization': `Bearer ${data.access}`, 'Content-Type': 'application/json', }, }); - } catch (err) { + } catch { throw new Error('SESSION_EXPIRED'); } } else { throw new Error('SESSION_EXPIRED'); } } - if (!res.ok) throw new Error('No autorizado o error en la petición'); - return res.json(); // Tipado por JSDoc: Promise + + if (!res.ok) throw new Error(await extractApiError(res)); + return res.json(); } -/** - * Obtiene los documentos por id de pedimento - * @param {string} token - * @param {string} id - * @returns {Promise} - */ + export async function fetchDocumentById(token, id) { let res = await fetch(`${API_URL}/record/documents/?page=1&page_size=10&pedimento=${id}/`, { headers: { @@ -76,27 +45,27 @@ export async function fetchDocumentById(token, id) { 'Content-Type': 'application/json', }, }); + if (res.status === 401) { - // Intentar refrescar el token const refresh = localStorage.getItem('refresh'); if (refresh) { try { const data = await refreshToken(refresh); localStorage.setItem('access', data.access); - // Reintenta la petición con el nuevo access token res = await fetch(`${API_URL}/record/documents/?page=1&page_size=10&pedimento=${id}/`, { headers: { 'Authorization': `Bearer ${data.access}`, 'Content-Type': 'application/json', }, }); - } catch (err) { + } catch { throw new Error('SESSION_EXPIRED'); } } else { throw new Error('SESSION_EXPIRED'); } } - if (!res.ok) throw new Error('No autorizado o error en la petición'); - return res.json(); // Tipado por JSDoc: Promise + + if (!res.ok) throw new Error(await extractApiError(res)); + return res.json(); } diff --git a/src/api/edocuments.js b/src/api/edocuments.js index 95e5d9b..8cf56fb 100644 --- a/src/api/edocuments.js +++ b/src/api/edocuments.js @@ -1,101 +1,58 @@ import { fetchWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; const API_BASE_URL = import.meta.env.VITE_EFC_API_URL; export const fetchPedimentoEdocuments = async (pedimentoId, page = 1, pageSize = 10, filters = {}) => { - try { - const params = new URLSearchParams({ - pedimento: pedimentoId, - page: page.toString(), - page_size: pageSize.toString(), - }); + const params = new URLSearchParams({ + pedimento: pedimentoId, + page: page.toString(), + page_size: pageSize.toString(), + }); - // Agregar filtros si existen - if (filters.numero_edocument) { - params.append('numero_edocument__icontains', filters.numero_edocument); - } - if (filters.clave) { - params.append('clave__icontains', filters.clave); - } - if (filters.descripcion) { - params.append('descripcion__icontains', filters.descripcion); - } - if (filters.edocument_descargado !== undefined && filters.edocument_descargado !== '') { - params.append('edocument_descargado', filters.edocument_descargado); - } - if (filters.acuse_descargado !== undefined && filters.acuse_descargado !== '') { - params.append('acuse_descargado', filters.acuse_descargado); - } - if (filters.date_from) { - params.append('created_at__gte', filters.date_from); - } - if (filters.date_to) { - params.append('created_at__lte', filters.date_to); - } + if (filters.numero_edocument) params.append('numero_edocument__icontains', filters.numero_edocument); + if (filters.clave) params.append('clave__icontains', filters.clave); + if (filters.descripcion) params.append('descripcion__icontains', filters.descripcion); + if (filters.edocument_descargado !== undefined && filters.edocument_descargado !== '') params.append('edocument_descargado', filters.edocument_descargado); + if (filters.acuse_descargado !== undefined && filters.acuse_descargado !== '') params.append('acuse_descargado', filters.acuse_descargado); + if (filters.date_from) params.append('created_at__gte', filters.date_from); + if (filters.date_to) params.append('created_at__lte', filters.date_to); - const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/?${params}`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/?${params}`); + if (!response.ok) throw new Error(await extractApiError(response)); - const data = await response.json(); - return { - results: data.results, - count: data.count, - next: data.next, - previous: data.previous - }; - } catch (error) { - console.error('Error fetching EDocs:', error); - throw error; - } + const data = await response.json(); + return { results: data.results, count: data.count, next: data.next, previous: data.previous }; }; export const downloadEdocument = async (edocId) => { - try { - const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/${edocId}/download/`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/${edocId}/download/`); + if (!response.ok) throw new Error(await extractApiError(response)); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `EDOC_${edocId}.pdf`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Error downloading EDocs:', error); - throw error; - } + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `EDOC_${edocId}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); }; export const downloadAcuseEdocument = async (edocId) => { - try { - const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/${edocId}/download-acuse/`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/${edocId}/download-acuse/`); + if (!response.ok) throw new Error(await extractApiError(response)); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `ACUSE_EDOC_${edocId}.pdf`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Error downloading EDocs acuse:', error); - throw error; - } -}; \ No newline at end of file + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `ACUSE_EDOC_${edocId}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); +}; diff --git a/src/api/expedientes.ts b/src/api/expedientes.ts index d062b00..67e0a21 100644 --- a/src/api/expedientes.ts +++ b/src/api/expedientes.ts @@ -33,9 +33,9 @@ export interface PedimentosFilters { aduana?: string; regimen?: string; clave_pedimento?: string; - fecha_inicio?: string; - fecha_fin?: string; fecha_pago?: string; + fecha_pago_desde?: string; + fecha_pago_hasta?: string; alerta?: string | boolean; agente_aduanal?: string; curp_apoderado?: string; @@ -53,6 +53,7 @@ export interface PedimentosFilters { contribuyente?: string; numero_edocs?: number; numero_coves?: number; + ordering?: string; } export async function fetchDocuments( diff --git a/src/api/organizacion.js b/src/api/organizacion.js index 272c95b..620fad6 100644 --- a/src/api/organizacion.js +++ b/src/api/organizacion.js @@ -1,4 +1,5 @@ import { refreshToken } from './auth'; +import { extractApiError } from './apiError'; const API_URL = import.meta.env.VITE_EFC_API_URL; @@ -9,14 +10,13 @@ export async function fetchOrganizationUsage(token) { 'Content-Type': 'application/json', }, }); + if (res.status === 401) { - // Intentar refrescar el token const refresh = localStorage.getItem('refresh'); if (refresh) { try { const data = await refreshToken(refresh); localStorage.setItem('access', data.access); - // Reintenta la petición con el nuevo access token res = await fetch(`${API_URL}/organization/uso-almacenamiento/mi_organizacion/`, { headers: { 'Authorization': `Bearer ${data.access}`, @@ -24,13 +24,14 @@ export async function fetchOrganizationUsage(token) { }, }); if (res.status === 401) throw new Error('SESSION_EXPIRED'); - } catch (err) { + } catch { throw new Error('SESSION_EXPIRED'); } } else { throw new Error('SESSION_EXPIRED'); } } - if (!res.ok) throw new Error('No autorizado o error en la petición'); + + if (!res.ok) throw new Error(await extractApiError(res)); return res.json(); } diff --git a/src/api/procesos.js b/src/api/procesos.js index 3e0476a..eab43a5 100644 --- a/src/api/procesos.js +++ b/src/api/procesos.js @@ -1,66 +1,32 @@ import { fetchWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; const API_BASE_URL = import.meta.env.VITE_EFC_API_URL; export const fetchPedimentoProcesos = async (pedimentoId, page = 1, pageSize = 10, filters = {}) => { - try { - const params = new URLSearchParams({ - pedimento: pedimentoId, - page: page.toString(), - page_size: pageSize.toString(), - }); + const params = new URLSearchParams({ + pedimento: pedimentoId, + page: page.toString(), + page_size: pageSize.toString(), + }); - // Agregar filtros si existen - if (filters.estado !== undefined && filters.estado !== '') { - params.append('estado', filters.estado); - } - if (filters.servicio !== undefined && filters.servicio !== '') { - params.append('servicio', filters.servicio); - } - if (filters.organizacion_name) { - params.append('organizacion_name__icontains', filters.organizacion_name); - } - if (filters.date_from) { - params.append('created_at__gte', filters.date_from); - } - if (filters.date_to) { - params.append('created_at__lte', filters.date_to); - } - if (filters.updated_from) { - params.append('updated_at__gte', filters.updated_from); - } - if (filters.updated_to) { - params.append('updated_at__lte', filters.updated_to); - } + if (filters.estado !== undefined && filters.estado !== '') params.append('estado', filters.estado); + if (filters.servicio !== undefined && filters.servicio !== '') params.append('servicio', filters.servicio); + if (filters.organizacion_name) params.append('organizacion_name__icontains', filters.organizacion_name); + if (filters.date_from) params.append('created_at__gte', filters.date_from); + if (filters.date_to) params.append('created_at__lte', filters.date_to); + if (filters.updated_from) params.append('updated_at__gte', filters.updated_from); + if (filters.updated_to) params.append('updated_at__lte', filters.updated_to); - const response = await fetchWithAuth(`${API_BASE_URL}/customs/procesamientopedimentos/?${params}`); - - if (!response.ok) { - throw new Error(`Error ${response.status}: ${response.statusText}`); - } + const response = await fetchWithAuth(`${API_BASE_URL}/customs/procesamientopedimentos/?${params}`); + if (!response.ok) throw new Error(await extractApiError(response)); - const data = await response.json(); - return { - results: data.results, - count: data.count, - next: data.next, - previous: data.previous - }; - } catch (error) { - console.error('Error fetching Procesos:', error); - throw error; - } + const data = await response.json(); + return { results: data.results, count: data.count, next: data.next, previous: data.previous }; }; -// Mapeo de estados export const getEstadoLabel = (estado) => { - const estados = { - 1: 'Pendiente', - 2: 'En Proceso', - 3: 'Completado', - 4: 'Error', - 5: 'Cancelado' - }; + const estados = { 1: 'Pendiente', 2: 'En Proceso', 3: 'Completado', 4: 'Error', 5: 'Cancelado' }; return estados[estado] || `Estado ${estado}`; }; @@ -70,38 +36,27 @@ export const getEstadoColor = (estado) => { 2: 'bg-blue-100 text-blue-800', 3: 'bg-green-100 text-green-800', 4: 'bg-red-100 text-red-800', - 5: 'bg-gray-100 text-gray-800' + 5: 'bg-gray-100 text-gray-800', }; return colores[estado] || 'bg-gray-100 text-gray-800'; }; -// Mapeo de servicios export const getServicioLabel = (servicio) => { const servicios = { - 1: 'Digitalización', - 2: 'Validación', - 3: 'Procesamiento SAT', - 4: 'Generación COVEs', - 5: 'Generación EDocs', - 6: 'Envío VUCEM', - 7: 'Clasificación', - 8: 'Archivo Digital', - 9: 'Notificaciones' + 1: 'Digitalización', 2: 'Validación', 3: 'Procesamiento SAT', + 4: 'Generación COVEs', 5: 'Generación EDocs', 6: 'Envío VUCEM', + 7: 'Clasificación', 8: 'Archivo Digital', 9: 'Notificaciones', }; return servicios[servicio] || `Servicio ${servicio}`; }; export const getServicioColor = (servicio) => { const colores = { - 1: 'bg-purple-100 text-purple-800', - 2: 'bg-indigo-100 text-indigo-800', - 3: 'bg-blue-100 text-blue-800', - 4: 'bg-cyan-100 text-cyan-800', - 5: 'bg-teal-100 text-teal-800', - 6: 'bg-green-100 text-green-800', - 7: 'bg-yellow-100 text-yellow-800', - 8: 'bg-orange-100 text-orange-800', - 9: 'bg-pink-100 text-pink-800' + 1: 'bg-purple-100 text-purple-800', 2: 'bg-indigo-100 text-indigo-800', + 3: 'bg-blue-100 text-blue-800', 4: 'bg-cyan-100 text-cyan-800', + 5: 'bg-teal-100 text-teal-800', 6: 'bg-green-100 text-green-800', + 7: 'bg-yellow-100 text-yellow-800', 8: 'bg-orange-100 text-orange-800', + 9: 'bg-pink-100 text-pink-800', }; return colores[servicio] || 'bg-gray-100 text-gray-800'; -}; \ No newline at end of file +}; diff --git a/src/api/rbac.js b/src/api/rbac.js new file mode 100644 index 0000000..303de77 --- /dev/null +++ b/src/api/rbac.js @@ -0,0 +1,153 @@ +const API_URL = import.meta.env.VITE_EFC_API_URL || 'http://localhost:8000'; +import { fetchWithAuth, postWithAuth, patchWithAuth, deleteWithAuth } from '../fetchWithAuth'; + +async function handleResponse(response, operation = 'operación') { + if (response.status === 401) throw new Error('SESSION_EXPIRED'); + if (!response.ok) { + let detail = `Error ${response.status}`; + try { + const body = await response.json(); + detail = body.detail || body.message || JSON.stringify(body); + } catch { + detail = await response.text().catch(() => detail); + } + throw new Error(detail); + } + const contentType = response.headers.get('content-type'); + if (!contentType?.includes('application/json')) return null; + return response.json(); +} + +// ── Catálogo de permisos ───────────────────────────────────────────────────── + +/** Array plano: [{ id, codename, descripcion, modulo }] */ +export async function fetchPermissions() { + const res = await fetchWithAuth(`${API_URL}/rbac/permissions/`); + return handleResponse(res, 'Fetch Permissions'); +} + +/** Agrupado por módulo: { cards: [...], coves: [...], ... } */ +export async function fetchPermissionsByModule() { + const res = await fetchWithAuth(`${API_URL}/rbac/permissions/by-module/`); + return handleResponse(res, 'Fetch Permissions By Module'); +} + +// ── Permisos efectivos del usuario actual ──────────────────────────────────── + +/** { permissions: ["cards.view", ...], roles: ["admin"] } */ +export async function fetchMyPermissions() { + const res = await fetchWithAuth(`${API_URL}/rbac/my-permissions/`); + return handleResponse(res, 'Fetch My Permissions'); +} + +// ── Roles (OrganizationRole) ───────────────────────────────────────────────── + +export async function fetchRoles() { + const res = await fetchWithAuth(`${API_URL}/rbac/roles/`); + return handleResponse(res, 'Fetch Roles'); +} + +/** + * data: { nombre, descripcion?, permission_ids: number[] } + * permission_ids son IDs numéricos de RolePermission. + */ +export async function createRole(data) { + const res = await postWithAuth(`${API_URL}/rbac/roles/`, data); + return handleResponse(res, 'Create Role'); +} + +export async function getRole(id) { + const res = await fetchWithAuth(`${API_URL}/rbac/roles/${id}/`); + return handleResponse(res, 'Get Role'); +} + +/** + * PATCH — reemplaza el rol completo. + * data: { nombre?, descripcion?, permission_ids?: number[] } + */ +export async function updateRole(id, data) { + const res = await patchWithAuth(`${API_URL}/rbac/roles/${id}/`, data); + return handleResponse(res, 'Update Role'); +} + +export async function deleteRole(id) { + const res = await deleteWithAuth(`${API_URL}/rbac/roles/${id}/`); + if (res.status === 401) throw new Error('SESSION_EXPIRED'); + if (!res.ok) { + let detail = `Error ${res.status}`; + try { detail = (await res.json()).detail || detail; } catch {} + throw new Error(detail); + } + return true; +} + +// ── Asignación de roles a usuarios (UserRole) ──────────────────────────────── + +/** + * userId: filtra por usuario. Usa ?user_id= según spec. + * Respuesta: [{ id, user: {...}, role: {...}, created_at }] + */ +export async function fetchUserRoles(userId) { + const url = userId + ? `${API_URL}/rbac/user-roles/?user_id=${userId}` + : `${API_URL}/rbac/user-roles/`; + const res = await fetchWithAuth(url); + return handleResponse(res, 'Fetch User Roles'); +} + +/** + * body: { user_id, role_id } — UUIDs según spec del backend. + */ +export async function assignUserRole(userId, roleId) { + const res = await postWithAuth(`${API_URL}/rbac/user-roles/`, { + user_id: userId, + role_id: roleId, + }); + return handleResponse(res, 'Assign Role'); +} + +export async function revokeUserRole(userRoleId) { + const res = await deleteWithAuth(`${API_URL}/rbac/user-roles/${userRoleId}/`); + if (res.status === 401) throw new Error('SESSION_EXPIRED'); + if (!res.ok) { + let detail = `Error ${res.status}`; + try { detail = (await res.json()).detail || detail; } catch {} + throw new Error(detail); + } + return true; +} + +// ── Switch de organización (solo SuperUser) ─────────────────────────────────── + +/** + * Establece la organización activa del superuser. + * body: { organization_id: "uuid" } + */ +export async function switchOrganization(organizationId) { + const res = await postWithAuth(`${API_URL}/rbac/switch-organization/`, { + organization_id: organizationId, + }); + return handleResponse(res, 'Switch Organization'); +} + +/** Limpia la organización activa del superuser. */ +export async function clearOrganization() { + const res = await deleteWithAuth(`${API_URL}/rbac/switch-organization/`); + if (res.status === 401) throw new Error('SESSION_EXPIRED'); + if (!res.ok) { + let detail = `Error ${res.status}`; + try { detail = (await res.json()).detail || detail; } catch {} + throw new Error(detail); + } + return true; +} + +// ── Permisos singulares por usuario (UserPermission) ───────────────────────── + +export async function fetchUserPermissions(userId) { + const url = userId + ? `${API_URL}/rbac/user-permissions/?user_id=${userId}` + : `${API_URL}/rbac/user-permissions/`; + const res = await fetchWithAuth(url); + return handleResponse(res, 'Fetch User Permissions'); +} diff --git a/src/api/users.js b/src/api/users.js index 03798d0..05136d6 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,56 +1,42 @@ const API_URL = import.meta.env.VITE_EFC_API_URL || 'http://localhost:8000'; import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; -// Función helper para manejar respuestas - -async function handleResponse(response, operation = 'operación') { - if (response.status === 401) { - throw new Error('SESSION_EXPIRED'); - } - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Error ${response.status}: ${response.statusText}`); - } +async function handleResponse(response) { + if (response.status === 401) throw new Error('SESSION_EXPIRED'); + if (!response.ok) throw new Error(await extractApiError(response)); const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - throw new Error('El servidor no devolvió JSON válido'); - } + if (!contentType || !contentType.includes('application/json')) return null; return response.json(); } export async function fetchUsers() { - const url = `${API_URL}/user/users/`; - const res = await fetchWithAuth(url); - return handleResponse(res, 'Fetch Users'); + const res = await fetchWithAuth(`${API_URL}/user/users/`); + return handleResponse(res); } export async function createUser(userData) { - const url = `${API_URL}/user/users/`; - const res = await postWithAuth(url, userData); - return handleResponse(res, 'Create User'); + const res = await postWithAuth(`${API_URL}/user/users/`, userData); + return handleResponse(res); } export async function updateUser(id, userData) { - const url = `${API_URL}/user/users/${id}/`; - const res = await putWithAuth(url, userData); - return handleResponse(res, 'Update User'); + const res = await putWithAuth(`${API_URL}/user/users/${id}/`, userData); + return handleResponse(res); } export async function deleteUser(id) { - const url = `${API_URL}/user/users/${id}/`; - const res = await deleteWithAuth(url); - if (!res.ok) throw new Error(`Error ${res.status}: ${res.statusText}`); - return true; + const res = await deleteWithAuth(`${API_URL}/user/users/${id}/`); + return handleResponse(res); } export async function getCurrentUser(token) { - const url = `${API_URL}/user/users/me/`; - const res = await fetch(url, { + const res = await fetch(`${API_URL}/user/users/me/`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }, }); - return handleResponse(res, 'Get Current User'); + return handleResponse(res); } diff --git a/src/api/users.ts b/src/api/users.ts index 580d3a4..f975339 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,138 +1,39 @@ +/// const API_URL = import.meta.env.VITE_EFC_API_URL || 'http://localhost:8000'; import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth } from '../fetchWithAuth'; +import { extractApiError } from './apiError'; -// Función helper para manejar respuestas -async function handleResponse(response, operation = 'operación') { - console.log(`📡 ${operation} response:`, response.status, response.statusText); - - if (response.status === 401) { - console.error('❌ Unauthorized - session expired'); - throw new Error('SESSION_EXPIRED'); - } - - if (!response.ok) { - const errorText = await response.text(); - console.error(`❌ ${operation} error:`, response.status, errorText); - throw new Error(`Error ${response.status}: ${response.statusText}`); - } - - // Verificar que la respuesta es JSON +async function handleResponse(response: Response) { + if (response.status === 401) throw new Error('SESSION_EXPIRED'); + if (!response.ok) throw new Error(await extractApiError(response)); const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - const text = await response.text(); - console.error('❌ Response is not JSON:', text.substring(0, 200)); - throw new Error('El servidor no devolvió JSON válido'); - } - + if (!contentType?.includes('application/json')) return null; return response.json(); } export async function fetchUsers() { - try { - const url = `${API_URL}/user/users/`; - console.log('👥 Fetching users from:', url); - - const res = await fetchWithAuth(url); - - const data = await handleResponse(res, 'Fetch Users'); - console.log('✅ Users data received'); - return data; - - } catch (error) { - console.error('❌ Error in fetchUsers:', error); - if (error.name === 'TypeError' && error.message.includes('fetch')) { - throw new Error('Error de conexión al servidor'); - } - throw error; - } + const res = await fetchWithAuth(`${API_URL}/user/users/`); + return handleResponse(res); } -export async function createUser(userData) { - try { - const url = `${API_URL}/user/users/`; - console.log('➕ Creating user at:', url); - - const res = await postWithAuth(url, userData); - - const data = await handleResponse(res, 'Create User'); - console.log('✅ User created successfully'); - return data; - - } catch (error) { - console.error('❌ Error in createUser:', error); - if (error.name === 'TypeError' && error.message.includes('fetch')) { - throw new Error('Error de conexión al servidor'); - } - throw error; - } +export async function createUser(userData: object) { + const res = await postWithAuth(`${API_URL}/user/users/`, userData); + return handleResponse(res); } -export async function updateUser(id, userData) { - try { - const url = `${API_URL}/user/users/${id}/`; - console.log('✏️ Updating user at:', url); - - const res = await putWithAuth(url, userData); - - const data = await handleResponse(res, 'Update User'); - console.log('✅ User updated successfully'); - return data; - - } catch (error) { - console.error('❌ Error in updateUser:', error); - if (error.name === 'TypeError' && error.message.includes('fetch')) { - throw new Error('Error de conexión al servidor'); - } - throw error; - } +export async function updateUser(id: string | number, userData: object) { + const res = await putWithAuth(`${API_URL}/user/users/${id}/`, userData); + return handleResponse(res); } -export async function deleteUser(id) { - try { - const url = `${API_URL}/user/users/${id}/`; - console.log('🗑️ Deleting user at:', url); - - const res = await deleteWithAuth(url); - - if (res.status === 401) { - console.error('❌ Unauthorized - session expired'); - throw new Error('SESSION_EXPIRED'); - } - - if (!res.ok) { - const errorText = await res.text(); - console.error('❌ Delete User error:', res.status, errorText); - throw new Error(`Error ${res.status}: ${res.statusText}`); - } - - console.log('✅ User deleted successfully'); - return true; // DELETE suele no devolver contenido - - } catch (error) { - console.error('❌ Error in deleteUser:', error); - if (error.name === 'TypeError' && error.message.includes('fetch')) { - throw new Error('Error de conexión al servidor'); - } - throw error; - } +export async function deleteUser(id: string | number) { + const res = await deleteWithAuth(`${API_URL}/user/users/${id}/`); + if (res.status === 401) throw new Error('SESSION_EXPIRED'); + if (!res.ok) throw new Error(await extractApiError(res)); + return true; } export async function getCurrentUser() { - try { - const url = `${API_URL}/user/users/me/`; - console.log('👤 Fetching current user from:', url); - - const res = await fetchWithAuth(url); - - const data = await handleResponse(res, 'Get Current User'); - console.log('✅ Current user data received:', data); - return data; - - } catch (error) { - console.error('❌ Error in getCurrentUser:', error); - if (error.name === 'TypeError' && error.message.includes('fetch')) { - throw new Error('Error de conexión al servidor'); - } - throw error; - } + const res = await fetchWithAuth(`${API_URL}/user/users/me/`); + return handleResponse(res); } diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 4d37f98..7994cb8 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,23 +1,21 @@ import React, { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useUser } from '../context/UserContext'; +import { fetchWithAuth } from '../fetchWithAuth'; export default function Sidebar({ isMobileOpen, onMobileClose }) { - // Leer si el usuario es importador desde localStorage - const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'; - // Leer grupos del usuario desde localStorage - let userGroups = []; - if (typeof window !== 'undefined') { - try { - userGroups = JSON.parse(localStorage.getItem('user_groups') || '[]'); - } catch { - userGroups = []; - } - } - // Si los grupos son exactamente [3,5] - const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5); - // Leer DEBUG_MODE desde variables de entorno const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true'; + + // Permisos RBAC — cargados desde /rbac/my-permissions/ al hacer login + const [userPermissions, setUserPermissions] = useState(() => { + try { + return JSON.parse(localStorage.getItem('user_permissions') || '[]'); + } catch { + return []; + } + }); + + const hasPermission = (codename) => userPermissions.includes(codename); // Estados para responsividad const [isCollapsed, setIsCollapsed] = useState(false); @@ -34,6 +32,14 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { const handleLogout = () => { localStorage.removeItem('access'); localStorage.removeItem('refresh'); + localStorage.removeItem('user_id'); + localStorage.removeItem('user_is_importador'); + localStorage.removeItem('user_groups'); + localStorage.removeItem('user_permissions'); + localStorage.removeItem('username'); + localStorage.removeItem('user_email'); + localStorage.removeItem('user_first_name'); + localStorage.removeItem('user_last_name'); window.dispatchEvent(new CustomEvent('authStateChanged')); navigate('/login'); }; @@ -55,6 +61,22 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { handleMobileClose(); }, [location.pathname]); + // Si no hay permisos en localStorage (sesión previa al RBAC), los carga del servidor + useEffect(() => { + if (userPermissions.length === 0 && localStorage.getItem('access')) { + const apiUrl = import.meta.env.VITE_EFC_API_URL || ''; + fetchWithAuth(`${apiUrl}/rbac/my-permissions/`) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data && Array.isArray(data.permissions)) { + localStorage.setItem('user_permissions', JSON.stringify(data.permissions)); + setUserPermissions(data.permissions); + } + }) + .catch(() => {}); + } + }, []); + // El usuario y loading ahora vienen del contexto global // Definir todas las secciones @@ -71,9 +93,8 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { ) }, - // Ocultar 'Mi Organización' si es importador o si esGroup35 ...( - (!isImportador && !isGroup35) + hasPermission('organizacion.view') ? [ { name: 'Mi Organización', @@ -101,15 +122,19 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { ) }, - { - name: 'Auditor', - path: '/auditor', - icon: ( - - - - ) - } + ...( + hasPermission('auditoria.view') + ? [{ + name: 'Auditor', + path: '/auditor', + icon: ( + + + + ) + }] + : [] + ) ] }, { @@ -158,9 +183,9 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { } ] }, - // Nueva sección Tableros - Solo mostrar si DEBUG_MODE es true + // Nueva sección Tableros - Solo mostrar si DEBUG_MODE es true y tiene cards.view ...( - (isDebugMode && !isGroup35) + (isDebugMode && hasPermission('cards.view')) ? [ { title: 'Tableros', @@ -179,71 +204,70 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { ] : [] ), - ...( - isGroup35 - ? [] - : [ - { - title: 'Acceso a Usuarios', - items: [ - // Botón Importadores como primer elemento - { - name: 'Importadores', - path: '/importers', - icon: ( - - - - ) - }, - ...( - isImportador - ? [] - : [ - { - name: 'Usuarios', - path: '/users', - icon: ( - - - - ) - } - ] - ), - { - name: 'Ventanilla Única', - path: '/vucem', - icon: ( - - - - - ) - } - ] - } - ] - ) + { + title: 'Acceso a Usuarios', + items: [ + ...( + hasPermission('importadores.view') + ? [{ + name: 'Importadores', + path: '/importers', + icon: ( + + + + ) + }] + : [] + ), + ...( + hasPermission('usuarios.view') + ? [{ + name: 'Usuarios', + path: '/users', + icon: ( + + + + ) + }] + : [] + ), + ...( + hasPermission('usuarios.manage_roles') + ? [{ + name: 'Perfiles', + path: '/profiles', + icon: ( + + + + ) + }] + : [] + ), + ...( + hasPermission('vucem.view') + ? [{ + name: 'Ventanilla Única', + path: '/vucem', + icon: ( + + + + + ) + }] + : [] + ), + ] + } ]; - // Filtrar secciones según si es importador y modo debug - // Modificar items según si es importador - const menuSections = allMenuSections - .map(section => { - if (section.title === 'Organización') { - return { - ...section, - items: section.items.filter(item => !(isImportador && item.name === 'Mi Organización')) - }; - } - // Para Tableros, filtrar la sección si es importador o si no está en modo debug - if (section.title === 'Tableros' && (isImportador || !isDebugMode)) { - return null; - } - return section; - }) - .filter(Boolean); + // Ocultar secciones que no tienen ningún item visible + const menuSections = allMenuSections.filter( + section => section && section.items && section.items.length > 0 + ); return ( <> diff --git a/src/fetchWithAuth.js b/src/fetchWithAuth.js index 565a2c0..773a993 100644 --- a/src/fetchWithAuth.js +++ b/src/fetchWithAuth.js @@ -74,6 +74,7 @@ const refreshToken = async () => { localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); + localStorage.removeItem('user_permissions'); // Redirigir al login después de un pequeño delay setTimeout(() => { diff --git a/src/hooks/usePollTaskStatus.js b/src/hooks/usePollTaskStatus.js new file mode 100644 index 0000000..e891311 --- /dev/null +++ b/src/hooks/usePollTaskStatus.js @@ -0,0 +1,99 @@ +import { useState, useRef, useCallback } from 'react'; +import { fetchWithAuth } from '../fetchWithAuth'; + +const API_BASE_URL = import.meta.env.VITE_EFC_API_URL; + +// Estados que indican que la tarea ya terminó (no hay más que esperar) +const FINAL_STATES = new Set(['SUCCESS', 'FAILURE', 'completed', 'failed', 'cancelled']); + +/** + * Polling acotado de estado de tarea. + * + * Uso: + * const { taskState, polling, poll, reset } = usePollTaskStatus({ maxAttempts: 3, intervalMs: 2500 }); + * + * // Después de enviar la tarea al microservicio: + * poll(taskId); + * + * // taskState: null | { status, message, error, attempts } + * // polling: true mientras hay intentos pendientes + */ +export function usePollTaskStatus({ maxAttempts = 3, intervalMs = 2500 } = {}) { + const [taskState, setTaskState] = useState(null); + const [polling, setPolling] = useState(false); + + const timeoutRef = useRef(null); + const abortedRef = useRef(false); + const attemptsRef = useRef(0); + + const stop = useCallback(() => { + abortedRef.current = true; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setPolling(false); + }, []); + + const reset = useCallback(() => { + stop(); + setTaskState(null); + attemptsRef.current = 0; + }, [stop]); + + const poll = useCallback((taskId) => { + if (!taskId) return; + + // Reiniciar estado previo + abortedRef.current = false; + attemptsRef.current = 0; + setPolling(true); + setTaskState({ status: 'PENDING', message: null, error: null, attempts: 0 }); + + const attempt = async () => { + if (abortedRef.current) return; + + attemptsRef.current += 1; + const n = attemptsRef.current; + + try { + const res = await fetchWithAuth(`${API_BASE_URL}/tasks/status/${taskId}/`); + if (abortedRef.current) return; + + if (!res.ok) { + setTaskState({ status: 'FAILURE', message: `Error HTTP ${res.status}`, error: true, attempts: n }); + setPolling(false); + return; + } + + const data = await res.json(); + if (abortedRef.current) return; + + const newState = { + status: data.status, + message: data.message || data.error || null, + error: data.error || null, + result: data.result || null, + attempts: n, + }; + setTaskState(newState); + + if (FINAL_STATES.has(data.status) || n >= maxAttempts) { + setPolling(false); + return; + } + + timeoutRef.current = setTimeout(attempt, intervalMs); + + } catch (err) { + if (abortedRef.current) return; + setTaskState({ status: 'FAILURE', message: err.message, error: true, attempts: n }); + setPolling(false); + } + }; + + attempt(); + }, [maxAttempts, intervalMs]); + + return { taskState, polling, poll, stop, reset }; +} \ No newline at end of file diff --git a/src/pages/Auditor.jsx b/src/pages/Auditor.jsx index 452a463..08d52d7 100644 --- a/src/pages/Auditor.jsx +++ b/src/pages/Auditor.jsx @@ -8,6 +8,7 @@ import xml from 'highlight.js/lib/languages/xml'; import 'highlight.js/styles/github.css'; hljs.registerLanguage('xml', xml); import { downloadFile } from '../utils/downloadUtils'; +import { extractApiError } from '../api/apiError'; const API_URL = import.meta.env.VITE_EFC_API_URL; @@ -342,10 +343,10 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP {/* Tabla de pedimentos con pendientes */} - {resultado.detalle_pendientes && resultado.detalle_pendientes.length > 0 && ( + {resultado.result?.detalle_pendientes && resultado.result.detalle_pendientes.length > 0 && (
- Pedimentos con pendientes ({resultado.detalle_pendientes.length}) + Pedimentos con pendientes ({resultado.result.detalle_pendientes.length}) @@ -355,14 +356,16 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP Pedimento - Documentos faltantes + Faltantes Progreso - - {resultado.detalle_pendientes.map((item, i) => { - const faltantesEntries = Object.entries(item).filter(([k]) => k.startsWith('faltantes_')); + + {resultado.result.detalle_pendientes.map((item, i) => { + const faltantesEntries = Object.entries(item).filter(([k]) => k.startsWith('faltantes_') || k === 'no_descargadas'); const todosLosIds = faltantesEntries.flatMap(([, v]) => Array.isArray(v) ? v : []); + const descargados = item.descargados ?? item.descargadas ?? '?'; + const total = item.total ?? item.total_partidas ?? '?'; return ( @@ -381,7 +384,7 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP - {item.descargados ?? '?'}/{item.total ?? '?'} + {descargados}/{total} ); @@ -393,10 +396,10 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP )} {/* Tabla de errores */} - {resultado.detalle_errores && resultado.detalle_errores.length > 0 && ( + {resultado.result?.detalle_errores && resultado.result.detalle_errores.length > 0 && (
- Errores ({resultado.detalle_errores.length}) + Errores ({resultado.result.detalle_errores.length}) @@ -410,7 +413,7 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP - {resultado.detalle_errores.map((item, i) => ( + {resultado.result.detalle_errores.map((item, i) => ( {item.pedimento} {item.error || '—'} @@ -560,8 +563,7 @@ function Auditor() { organizacionid: globalAuditModal.organizacion_id, }); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || errorData.error || `Error ${response.status}`); + throw new Error(await extractApiError(response)); } const data = await response.json(); showMessage(data.message || 'Proceso iniciado correctamente', 'success'); @@ -585,8 +587,7 @@ function Auditor() { setIniciandoProcesamiento(true); const response = await postWithAuth(`${API_URL}/customs/pedimentos/${pedimento_id}/${urlPath}/`); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || errorData.error || `Error ${response.status}`); + throw new Error(await extractApiError(response)); } const data = await response.json(); showMessage(data.status || 'Procesamiento iniciado correctamente', 'success'); @@ -603,7 +604,7 @@ function Auditor() { try { setConsultandoTask(true); const response = await getWithAuth(`${API_URL}/tasks/status/${globalAuditModal.task_id}/`); - if (!response.ok) throw new Error('Error al consultar el estado de la tarea'); + if (!response.ok) throw new Error(await extractApiError(response)); const data = await response.json(); if (data.ready) { setGlobalAuditModal(prev => ({ ...prev, resultado: data })); @@ -630,7 +631,7 @@ function Auditor() { organizacion_id: organizacionId }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría de acuses'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setGlobalAuditModal({ label: 'Acuses', procesamiento: 'acuses', organizacion_id: organizacionId, task_id: data.task_id, resultado: null }); @@ -654,7 +655,7 @@ function Auditor() { organizacion_id: organizacionId }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría de EDocuments'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setGlobalAuditModal({ label: 'EDocuments', procesamiento: 'edocs', organizacion_id: organizacionId, task_id: data.task_id, resultado: null }); @@ -678,7 +679,7 @@ function Auditor() { organizacion_id: organizacionId }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría de acuses cove'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setGlobalAuditModal({ label: 'Acuses de COVE', procesamiento: 'acuse_coves', organizacion_id: organizacionId, task_id: data.task_id, resultado: null }); @@ -699,7 +700,7 @@ function Auditor() { pedimento_id: pedimentoId }); if (!response.ok) { - throw new Error('Error al auditar el pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setAuditResultModal({ tipo: 'pc', pedimento_id: pedimentoId, pedimento_app: pedimento?.pedimento_app, data }); @@ -722,7 +723,7 @@ function Auditor() { }); if (!response.ok) { - throw new Error('Error al procesar las remesas del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); @@ -750,7 +751,7 @@ function Auditor() { }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría de remesas'); + throw new Error(await extractApiError(response)); } const data = await response.json(); @@ -774,7 +775,7 @@ function Auditor() { }); if (!response.ok) { - throw new Error('Error al procesar las partidas del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); @@ -802,7 +803,7 @@ function Auditor() { }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría de partidas'); + throw new Error(await extractApiError(response)); } const data = await response.json(); @@ -831,7 +832,7 @@ function Auditor() { }); if (!response.ok) { - throw new Error('Error al iniciar la auditoría'); + throw new Error(await extractApiError(response)); } const data = await response.json(); @@ -853,7 +854,7 @@ function Auditor() { pedimento_id: pedimentoId }); if (!response.ok) { - throw new Error('Error al auditar acuse del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setAuditResultModal({ tipo: 'ac', pedimento_id: pedimentoId, pedimento_app: pedimento?.pedimento_app, data }); @@ -873,7 +874,7 @@ function Auditor() { pedimento_id: pedimentoId }); if (!response.ok) { - throw new Error('Error al auditar acuse cove del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setAuditResultModal({ tipo: 'ac_cove', pedimento_id: pedimentoId, pedimento_app: pedimento?.pedimento_app, data }); @@ -894,7 +895,7 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { pedimento_id: pedimentoId }); if (!response.ok) { - throw new Error('Error al auditar eDocument del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setAuditResultModal({ tipo: 'edoc', pedimento_id: pedimentoId, pedimento_app: pedimento?.pedimento_app, data }); @@ -914,7 +915,7 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { pedimento_id: pedimentoId }); if (!response.ok) { - throw new Error('Error al auditar COVE del pedimento'); + throw new Error(await extractApiError(response)); } const data = await response.json(); setAuditResultModal({ tipo: 'cove', pedimento_id: pedimentoId, pedimento_app: pedimento?.pedimento_app, data }); @@ -935,7 +936,7 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { pedimento_id: pedimentoId }); if (!response.ok) { - showMessage('Error al intentar realizar la peticion de pedimento VU', 'warning'); + showMessage(await extractApiError(response), 'error'); return; } @@ -1054,14 +1055,7 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró datos para auditoría: ${getVistaNombre(vista_auditar)}. Detalle: ${errorMessage}`, 'warning'); - - // showMessage(`Error al obtener datos para vista ${vista_auditar}`, 'error'); + showMessage(`No se encontró datos para auditoría: ${getVistaNombre(vista_auditar)}. Detalle: ${await extractApiError(response)}`, 'warning'); setDetalleModalXml(null); return; } @@ -1125,7 +1119,7 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${doc.id}/`); if (!res.ok) { - setPreviewError('Error al obtener el archivo'); + setPreviewError(await extractApiError(res)); setPreviewLoading(false); return; } @@ -1169,11 +1163,10 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { setPreviewLoading(false); } } catch (err) { - console.error('Error en vista previa:', err); if (err.message === 'SESSION_EXPIRED') { setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.'); } else { - setPreviewError('Error al obtener el archivo'); + setPreviewError(err.message || 'Error al obtener el archivo'); } setPreviewLoading(false); } @@ -1262,24 +1255,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - console.error('Error al obtener petición VU:', error); - showMessage('Error al cargar la petición VU', 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1292,24 +1277,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - console.error('Error al obtener petición VU:', error); - showMessage('Error al cargar la petición VU', 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1323,24 +1300,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - console.error('Error al obtener petición VU:', error); - showMessage('Error al cargar la petición VU', 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1353,24 +1322,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1384,23 +1345,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - showMessage(`Error al cargar la petición VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1413,24 +1367,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1443,24 +1389,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la petición VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1473,24 +1411,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1503,24 +1433,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la petición VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1533,24 +1455,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1564,26 +1478,19 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - showMessage(`Error al cargar la petición VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; - + // Vista previa de respuesta Acuse COVE VU const handlePreviewRespuestaAcuseCoveVU = async (pedimentoId) => { try { @@ -1593,24 +1500,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1624,23 +1523,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de petición VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de petición VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - showMessage(`Error al cargar la petición VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la petición VU', 'error'); } }; @@ -1653,24 +1545,16 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => { }); if (!response.ok) { - // Obtener el JSON de error que devuelve Django - const errorData = await response.json(); - - const errorMessage = errorData.error || - `Error ${response.status}: ${response.statusText}`; - - showMessage(`No se encontró archivo de respuesta VU: ${errorMessage}`, 'warning'); + showMessage(`No se encontró archivo de respuesta VU: ${await extractApiError(response)}`, 'warning'); return; } const documento = await response.json(); - // Llamar a la función de vista previa existente await previewDocument(documento); }catch(error) { - - showMessage(`Error al cargar la respuesta VU: ${error.message|| 'Error desconocido'}`, 'error'); + showMessage(error.message || 'Error al cargar la respuesta VU', 'error'); } }; @@ -1726,7 +1610,7 @@ function formatXml(xml) { ...(pedimentoFilter && { search: pedimentoFilter }) }); const response = await getWithAuth(`${API_URL}/customs/pedimentos/?${queryParams}`); - if (!response.ok) throw new Error('Error al cargar los pedimentos'); + if (!response.ok) throw new Error(await extractApiError(response)); const data = await response.json(); setPedimentos(data.results); setCount(data.count); diff --git a/src/pages/Datastage.jsx b/src/pages/Datastage.jsx index 36fbb0a..08bcac2 100644 --- a/src/pages/Datastage.jsx +++ b/src/pages/Datastage.jsx @@ -22,6 +22,8 @@ async function patchProcesadoTrue(item) { }); } import { fetchWithAuth } from '../fetchWithAuth'; +import { useNotification } from '../context/NotificationContext'; +import { extractApiError } from '../api/apiError'; // Modal para mostrar registros cargados function RegistrosCargadosModal({ open, onClose, registros }) { @@ -59,7 +61,7 @@ function RegistrosCargadosModal({ open, onClose, registros }) { } // Procesar datastage (adaptado para mostrar registros cargados) -async function procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal) { +async function procesarDatastage(item, setDatastages, setSuccess, showMessage, setRegistrosCargados, setShowRegistrosModal) { try { const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/procesar/`; const body = { @@ -72,53 +74,54 @@ async function procesarDatastage(item, setDatastages, setSuccess, setError, setR headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + if (!res.ok) { + showMessage(await extractApiError(res), 'error'); + return; + } const data = await res.json(); - if (res.status === 200) { - // PATCH para marcar como procesado en backend - await patchProcesadoTrue(item); - setDatastages(prev => ({ - ...prev, - results: Array.isArray(prev.results) - ? prev.results.map(d => d.id === item.id ? { ...d, procesado: true } : d) - : [] - })); - // Mostrar el mensaje con task_id y detail si existen - if (data && data.task_id && data.detail) { - setSuccess(`Procesamiento iniciado.\nTask ID: ${data.task_id}\n${data.detail}`); - } else { - setSuccess('Procesado correctamente'); - } - // El modal de éxito se debe mostrar en el componente principal después de setSuccess - // No se llama aquí - if (data && data.registros_cargados) { - setRegistrosCargados(data.registros_cargados); - setShowRegistrosModal(true); - } + await patchProcesadoTrue(item); + setDatastages(prev => ({ + ...prev, + results: Array.isArray(prev.results) + ? prev.results.map(d => d.id === item.id ? { ...d, procesado: true } : d) + : [] + })); + if (data && data.task_id && data.detail) { + setSuccess(`Procesamiento iniciado.\nTask ID: ${data.task_id}\n${data.detail}`); } else { - setError(data && data.detail ? data.detail : 'No se pudo procesar el datastage'); + setSuccess('Procesado correctamente'); + } + if (data && data.registros_cargados) { + setRegistrosCargados(data.registros_cargados); + setShowRegistrosModal(true); } } catch (e) { - setError('No se pudo procesar el datastage'); + showMessage(e.message || 'No se pudo procesar el datastage', 'error'); } } // Descarga autenticada de archivos datastage -function downloadDatastageFile(id, filename) { +async function downloadDatastageFile(id, filename, showMessage) { const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${id}/download-datastage/`; - fetchWithAuth(url, { method: 'GET' }) - .then(async res => { - if (!res.ok) throw new Error('Error al descargar archivo'); - const blob = await res.blob(); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = filename || `datastage_${id}.zip`; - document.body.appendChild(link); - link.click(); - link.remove(); - }) - .catch(() => alert('No se pudo descargar el archivo.')); + try { + const res = await fetchWithAuth(url, { method: 'GET' }); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } + const blob = await res.blob(); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename || `datastage_${id}.zip`; + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (err) { + showMessage(err.message || 'No se pudo descargar el archivo', 'error'); + } } export default function Datastage() { + const { showMessage } = useNotification(); const focusKeeperRef = useRef(null); // datastages will hold the full API response object (with .results and .count) const [datastages, setDatastages] = useState({ results: [], count: 0 }); @@ -165,6 +168,7 @@ export default function Datastage() { } } catch (e) { setError(e.message); + showMessage(e.message, 'error'); setDatastages({ results: [], count: 0 }); } setLoading(false); @@ -183,7 +187,7 @@ export default function Datastage() { setSelected(detail); setShowDetailModal(true); } catch (e) { - setError(e.message); + showMessage(e.message, 'error'); } setLoading(false); }; @@ -197,14 +201,16 @@ export default function Datastage() { const fd = new FormData(); fd.append('contribuyente', form.contribuyente); if (form.archivo) fd.append('archivo', form.archivo); - await postFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`, fd); + const res = await postFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`, fd); + if (!res.ok) throw new Error(await extractApiError(res)); setForm({ archivo: null, contribuyente: '' }); setShowCreateModal(false); setSuccess('Datastage creado exitosamente'); setShowSuccessModal(true); load(); } catch (e) { - setError(e.message); + setShowCreateModal(false); + showMessage(e.message || 'Error al crear el datastage', 'error'); } setLoading(false); }; @@ -218,7 +224,8 @@ export default function Datastage() { const fd = new FormData(); fd.append('contribuyente', form.contribuyente); if (form.archivo) fd.append('archivo', form.archivo); - await patchFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${editingId}/`, fd); + const res = await patchFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${editingId}/`, fd); + if (!res.ok) throw new Error(await extractApiError(res)); setForm({ archivo: null, contribuyente: '' }); setEditingId(null); setShowEditModal(false); @@ -226,7 +233,8 @@ export default function Datastage() { setShowSuccessModal(true); load(); } catch (e) { - setError(e.message); + setShowEditModal(false); + showMessage(e.message || 'Error al actualizar el datastage', 'error'); } setLoading(false); }; @@ -244,7 +252,8 @@ export default function Datastage() { setShowSuccessModal(true); load(); } catch (e) { - setError(e.message); + setShowDeleteModal(false); + showMessage(e.message || 'Error al eliminar el datastage', 'error'); } setLoading(false); }; @@ -269,8 +278,9 @@ export default function Datastage() { } else { setImportadores([]); } - } catch { + } catch (e) { setImportadores([]); + showMessage(e.message || 'Error al cargar importadores', 'error'); } setShowCreateModal(true); }; @@ -404,7 +414,8 @@ export default function Datastage() { } catch { return ''; } - })() + })(), + showMessage )} > @@ -444,7 +455,7 @@ export default function Datastage() {
- - setFechaPagoFilter(e.target.value)} - className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" /> + + { + setFechaInicioFilter(e.target.value); + setCurrentPage(1); + }} + max={fechaFinFilter || undefined} + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Fecha de fin */} +
+ + { + setFechaFinFilter(e.target.value); + setCurrentPage(1); + }} + min={fechaInicioFilter || undefined} + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + />
{/* Patente */}
@@ -1134,14 +1228,23 @@ const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMe Actualizar Ahora - +
@@ -1176,12 +1279,49 @@ const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMe className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" /> - Pedimento - Fecha Pago + handleSort('pedimento')} + title="Ordenar por pedimento" + > + Pedimento + + handleSort('fecha_pago')} + title="Ordenar por fecha de pago" + > + Fecha Pago + + handleSort('aduana')} + title="Ordenar por aduana" + > + Aduana + + handleSort('patente')} + title="Ordenar por patente" + > + Patente + Contribuyente CURP Apod. Partidas - F. Carga + handleSort('created_at')} + title="Ordenar por fecha de carga" + > + F. Carga + Tipo Op. Clave Archivos @@ -1233,6 +1373,8 @@ const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMe {ped.fecha_pago} + {ped.aduana ?? '—'} + {ped.patente ?? '—'} {ped.contribuyente} {ped.curp_apoderado} {ped.numero_partidas} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 40ef1dc..20e2c48 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -23,11 +23,12 @@ export default function Login() { const apiUrl = import.meta.env.VITE_EFC_API_URL || ''; const token = data.access; try { - const res = await fetch(`${apiUrl}/user/users/me/`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (res.ok) { - const user = await res.json(); + const [resUser, resPerms] = await Promise.all([ + fetch(`${apiUrl}/user/users/me/`, { headers: { 'Authorization': `Bearer ${token}` } }), + fetch(`${apiUrl}/rbac/my-permissions/`, { headers: { 'Authorization': `Bearer ${token}` } }), + ]); + if (resUser.ok) { + const user = await resUser.json(); if (user && user.username) { localStorage.setItem('username', user.username); if (user.email) localStorage.setItem('user_email', user.email); @@ -38,6 +39,12 @@ export default function Login() { if (typeof user.is_importador !== 'undefined') localStorage.setItem('user_is_importador', String(user.is_importador)); } } + if (resPerms.ok) { + const permsData = await resPerms.json(); + if (permsData && permsData.permissions) { + localStorage.setItem('user_permissions', JSON.stringify(permsData.permissions)); + } + } } catch (e) { // Si falla, continuar igual console.error('No se pudo guardar info de usuario en localStorage', e); diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index 7b5b5d6..55bc4d4 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -136,7 +136,9 @@ export default function PedimentoDetail() { const [isSelectAllDocs, setIsSelectAllDocs] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showUploadModal, setShowUploadModal] = useState(false); - + const [headerUploadTypeId, setHeaderUploadTypeId] = useState(null); + const [headerUploadNumero, setHeaderUploadNumero] = useState(''); + // Estados para subir documentos const [selectedFiles, setSelectedFiles] = useState([]); const [uploadingDocuments, setUploadingDocuments] = useState(false); @@ -983,7 +985,12 @@ const handleDeleteSelectedPedimentoDocuments = async () => { // Agregar el ID del pedimento formData.append('pedimento_id', id); - + + // Tipo de documento específico si la subida viene del header de una sección + if (headerUploadTypeId) { + formData.append('document_type_id', headerUploadTypeId); + } + // Agregar archivos al FormData selectedFiles.forEach((file) => { formData.append('files', file); @@ -1001,6 +1008,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => { // Limpiar archivos seleccionados y cerrar modal setSelectedFiles([]); setShowUploadModal(false); + setHeaderUploadTypeId(null); // Forzar recarga de documentos const currentPage = page; @@ -2815,7 +2823,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => { return; } - const result = await postFormDataWithAuth(`${API_URL}/record/documents/bulk-upload-vu/`, formData); + const result = await postFormDataWithAuth(`${API_URL}/record/documents/bulk-upload/`, formData); if(!result.ok){ const errorData = await result.json(); @@ -2840,7 +2848,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => { }catch (error){ showMessage(`Error durante la subida: ${error.message}`, 'error'); } finally { - + // Limpiar y cerrar setSelectedDocumentForUpload(null); setUploadFiles([]); @@ -2855,6 +2863,62 @@ const isPartida = selectedDocumentForUpload?.tab === 'partida'; const isCove = selectedDocumentForUpload?.tab === 'cove'; const isEdoc = selectedDocumentForUpload?.tab === 'edoc'; + // Función para crear un registro VU nuevo (partida/cove/edoc) y subir sus archivos + const handleCreateVuRecord = async () => { + const tab = selectedDocumentForUpload?.tab; + if (!tab || !headerUploadNumero.trim() || uploadFiles.length === 0) { + showMessage('Por favor completa el número y selecciona al menos un archivo', 'warning'); + return; + } + + setUploadingToDocumento('header-upload'); + + try { + const formData = new FormData(); + formData.append('pedimento_id', id); + formData.append('tab_seccion', tab); + formData.append('numero', headerUploadNumero.trim()); + + uploadFiles.forEach((fileObj, index) => { + formData.append('files', fileObj.file); + formData.append(`secciones[${index}]`, fileObj.seccion); + }); + + const result = await postFormDataWithAuth(`${API_URL}/record/documents/create-vu-record/`, formData); + + if (!result.ok) { + const errorData = await result.json(); + throw new Error(errorData.error || 'Error desconocido'); + } + + const data = await result.json(); + showMessage(data.message || 'Registro creado exitosamente', 'success'); + + // Recargar la tabla correspondiente + if (tab === 'partida') { + fetchPedimentoPartidas(partidasPage, partidasPageSize, partidasFilters) + .then((d) => { setPartidas(d.results || []); setPartidasCount(d.count || 0); }) + .catch(err => console.error('Error recargando partidas:', err)); + } else if (tab === 'cove') { + fetchPedimentoCoves && fetchPedimentoCoves(covesPage, covesPageSize, covesFilters) + .then((d) => { setCoves(d.results || []); setCovesCount(d.count || 0); }) + .catch(err => console.error('Error recargando coves:', err)); + } else if (tab === 'edoc') { + fetchPedimentoEdocs && fetchPedimentoEdocs(edocsPage, edocsPageSize, edocsFilters) + .then((d) => { setEdocs(d.results || []); setEdocsCount(d.count || 0); }) + .catch(err => console.error('Error recargando edocs:', err)); + } + } catch (error) { + showMessage(`Error: ${error.message}`, 'error'); + } finally { + setSelectedDocumentForUpload(null); + setUploadFiles([]); + setUploadModalOpen(false); + setHeaderUploadNumero(''); + setUploadingToDocumento(null); + } + }; + // Estados para documentos de errores VU const [errorDocuments, setErrorDocuments] = useState([]); const [errorDocsCount, setErrorDocsCount] = useState(0); @@ -4324,7 +4388,24 @@ useEffect(() => { {partidasCount} partidas - + +
+ +
+ {/* Botones de acción masiva */} {/*
{selectedPartidas.length > 0 && ( @@ -4811,6 +4892,23 @@ useEffect(() => { {covesCount} COVEs
+ +
+ +
{/* Botones para descargar todos los AcuseCoves y COVEs */} @@ -5265,6 +5363,21 @@ useEffect(() => { {edocsCount} EDocs + + {/* Filtros */} @@ -6947,6 +7060,7 @@ useEffect(() => { onClick={() => { setShowUploadModal(false); setSelectedFiles([]); + setHeaderUploadTypeId(null); }} className="px-4 py-2 text-sm font-medium text-gray-700 transition-colors duration-200 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" > @@ -7158,15 +7272,34 @@ useEffect(() => {

- Subir documentos a {selectedDocumentForUpload.tab}: {selectedDocumentForUpload.numero} + {selectedDocumentForUpload.isHeaderUpload + ? `Nuevo ${selectedDocumentForUpload.tab === 'partida' ? 'Partida' : selectedDocumentForUpload.tab === 'cove' ? 'COVE' : 'EDoc'}` + : `Subir documentos a ${selectedDocumentForUpload.tab}: ${selectedDocumentForUpload.numero}`}

- Selecciona los archivos que deseas subir + {selectedDocumentForUpload.isHeaderUpload + ? 'Se creará el registro y se subirán los archivos' + : 'Selecciona los archivos que deseas subir'}

{/* Selectores específicos por tipo de archivo */}
+ {/* Input de número para header upload */} + {selectedDocumentForUpload.isHeaderUpload && ( +
+ + setHeaderUploadNumero(e.target.value)} + placeholder={isPartida ? 'Ej. 1' : isCove ? 'Ej. CV-2024-001' : 'Ej. ED-2024-001'} + className="block w-full px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+ )} {/* Primer selector para COVE (XML solo para partida y cove) */} {(isPartida || isCove) && (
@@ -7256,14 +7389,23 @@ useEffect(() => { setUploadModalOpen(false); setShowUploadModal(false); setSelectedFiles([]); + setHeaderUploadNumero(''); }} className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" > Cancelar +
+ + + +
+
+ +
+ + {/* Datos del perfil */} +
+
+
+ + + +
+
+

Datos del perfil

+

Identifica el perfil dentro de la organización

+
+
+
+
+ + setName(e.target.value)} + required + disabled={isAdminRole} + placeholder="Ej. Operador de VUCEM" + className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white text-slate-900 placeholder-slate-400 text-sm disabled:bg-slate-100 disabled:text-slate-500" + /> + {isAdminRole && ( +

+ + + + Perfil de administrador — el nombre es fijo +

+ )} +
+
+ + setDescription(e.target.value)} + placeholder="Descripción opcional del perfil" + className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white text-slate-900 placeholder-slate-400 text-sm" + /> +
+
+
+ + {/* Permisos */} +
+
+
+
+ + + +
+
+

Permisos

+

+ {loadingPerms + ? 'Cargando catálogo…' + : `${selectedPermIds.size} de ${totalPerms} permisos seleccionados`} +

+
+
+ {!loadingPerms && ( +
+ + +
+ )} +
+ + {loadingPerms ? ( +
+
+
+ ) : modules.length === 0 ? ( +
+ No se pudo cargar el catálogo de permisos +
+ ) : ( +
+ {modules.map(module => { + const modulePerms = permsByModule[module] || []; + const selectedCount = modulePerms.filter(p => selectedPermIds.has(p.id)).length; + const allSelected = selectedCount === modulePerms.length && modulePerms.length > 0; + const someSelected = selectedCount > 0 && !allSelected; + + return ( +
+ {/* Cabecera módulo */} +
toggleModule(module)} + > +
+
+ {allSelected && ( + + + + )} + {someSelected && ( +
+ )} +
+ + {MODULE_LABELS[module] ?? module} + +
+ + {selectedCount}/{modulePerms.length} + +
+ + {/* Permisos del módulo */} +
+ {modulePerms.map(perm => { + const active = selectedPermIds.has(perm.id); + return ( + + ); + })} +
+
+ ); + })} +
+ )} +
+ + {/* Botones */} +
+ + +
+ +
+
+ ); +} diff --git a/src/pages/Profiles.jsx b/src/pages/Profiles.jsx new file mode 100644 index 0000000..f20043c --- /dev/null +++ b/src/pages/Profiles.jsx @@ -0,0 +1,275 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { fetchRoles, deleteRole } from '../api/rbac'; +import { useNotification } from '../context/NotificationContext'; + +export default function Profiles() { + const [roles, setRoles] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [roleToDelete, setRoleToDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + const navigate = useNavigate(); + const { showMessage } = useNotification(); + + useEffect(() => { + if (typeof window !== 'undefined' && !document.getElementById('profiles-animations')) { + const style = document.createElement('style'); + style.id = 'profiles-animations'; + style.innerHTML = ` + @keyframes fadeInUpProfiles { + 0% { opacity: 0; transform: translateY(24px); } + 100% { opacity: 1; transform: translateY(0); } + } + .fade-in-up-profiles { animation: fadeInUpProfiles 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; } + `; + document.head.appendChild(style); + } + }, []); + + const loadRoles = () => { + setLoading(true); + fetchRoles() + .then(data => { + setRoles(Array.isArray(data) ? data : (data?.results ?? [])); + setLoading(false); + }) + .catch(err => { + showMessage(err.message || 'Error al cargar perfiles', 'error'); + setLoading(false); + }); + }; + + useEffect(() => { loadRoles(); }, []); + + const handleDeleteClick = (role) => { + setRoleToDelete(role); + setShowDeleteModal(true); + }; + + const handleDeleteConfirm = async () => { + if (!roleToDelete) return; + setDeleting(true); + try { + await deleteRole(roleToDelete.id); + showMessage('Perfil eliminado correctamente', 'success'); + setShowDeleteModal(false); + setRoleToDelete(null); + loadRoles(); + } catch (err) { + showMessage(err.message || 'Error al eliminar perfil', 'error'); + } finally { + setDeleting(false); + } + }; + + const filtered = roles.filter(r => + r.nombre?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const permCount = (role) => { + if (Array.isArray(role.permissions)) return role.permissions.length; + return '—'; + }; + + return ( +
+
+ + {/* Header */} +
+
+ + + +
+
+

+ Perfiles y Permisos +

+

+ Gestión de roles y accesos de la organización +

+
+ +
+ + + +
+
+ + {/* Buscador */} +
+
+ + + + setSearchTerm(e.target.value)} + placeholder="Buscar perfil..." + className="w-full pl-9 pr-4 py-2 border border-slate-200 rounded-lg shadow-sm bg-white text-sm text-slate-800 placeholder-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" + /> +
+
+ + {/* Tabla */} +
+ {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ + + +

+ {searchTerm ? 'Sin resultados para la búsqueda' : 'No hay perfiles registrados'} +

+
+ ) : ( +
+ + + + + + + + + + + {filtered.map(role => ( + + + + + + + ))} + +
PerfilTipoPermisosAcciones
+
+
+ {role.is_admin_role ? ( + + + + ) : ( + + + + )} +
+ {role.nombre} +
+
+ {role.is_admin_role ? ( + + + + + Administrador + + ) : ( + + Estándar + + )} + + + + + + {permCount(role)} + + +
+ + +
+
+
+ )} +
+ + {/* Contador */} + {!loading && filtered.length > 0 && ( +

+ {filtered.length} perfil{filtered.length !== 1 ? 'es' : ''} +

+ )} +
+ + {/* Modal confirmar eliminación */} + {showDeleteModal && roleToDelete && ( +
+
+
+
+ + + +
+
+

Eliminar perfil

+

Esta acción no se puede deshacer

+
+
+

+ ¿Confirmas eliminar el perfil "{roleToDelete.nombre}"? + Los usuarios con este perfil perderán los permisos asociados. +

+
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/pages/Reports.jsx b/src/pages/Reports.jsx index dc09619..d57846e 100644 --- a/src/pages/Reports.jsx +++ b/src/pages/Reports.jsx @@ -9,6 +9,7 @@ const fetchCurrentUserWithAuth = async () => { }; import { fetchWithAuth } from '../fetchWithAuth'; import { useNotification } from '../context/NotificationContext'; +import { extractApiError } from '../api/apiError'; import datastageModelsData from '../data/datastageModels.json'; import pedimentosModelsData from '../data/pedimentosModels.json'; @@ -42,27 +43,6 @@ if (typeof document !== 'undefined' && !document.getElementById('reports-animati document.head.appendChild(style); } -const handleDownloadReport = async (reportId) => { - try { - const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-download/${reportId}/`; - const res = await fetchWithAuth(url); - if (!res.ok) throw new Error('Error al descargar el reporte'); - const blob = await res.blob(); - let filename = `reporte_${reportId}.csv`; - const disposition = res.headers.get('Content-Disposition'); - if (disposition && disposition.includes('filename=')) { - filename = disposition.split('filename=')[1].replace(/"/g, '').trim(); - } - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } catch (err) { - alert('No se pudo descargar el reporte.'); - } -}; export default function Reports() { // Estado para organizacion_id @@ -85,7 +65,7 @@ export default function Reports() { // Handler for Generar Reporte in Cumplimiento tab const handleGenerarReporteCumplimiento = async () => { if (!organizacionId) { - alert('No se pudo obtener el organizacion_id. Intenta de nuevo más tarde.'); + showMessage('No se pudo obtener el ID de organización. Intenta de nuevo más tarde.', 'warning'); return; } // Build query params from filtersCumplimiento and add organizacion_id @@ -97,11 +77,13 @@ export default function Reports() { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/table-summary/${params ? `?${params}` : ''}`; try { const res = await fetchWithAuth(url); - const data = await res.json(); - if (!res.ok) throw new Error('Error al generar el reporte'); - alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.'); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } + showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success'); } catch (err) { - alert('No se pudo generar el reporte.'); + showMessage(err.message || 'No se pudo generar el reporte', 'error'); } }; // Filtros replicados de TableroAlmacenamiento @@ -143,8 +125,8 @@ export default function Reports() { // Build query params from filtersCumplimiento and add organizacion_id const paramsObj = { ...filtersControlPedimento }; - if(paramsObj.organizacion_id == ''){ - alert('No se pudo obtener el organizacion_id. Selecciona tu organizacion para intenta de nuevo.'); + if (paramsObj.organizacion_id === '') { + showMessage('Selecciona tu organización antes de generar el reporte.', 'warning'); return; } @@ -155,11 +137,13 @@ export default function Reports() { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/control-pedimento/${params ? `?${params}` : ''}`; try { const res = await fetchWithAuth(url); - const data = await res.json(); - if (!res.ok) throw new Error('Error al generar el reporte'); - alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.'); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } + showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success'); } catch (err) { - alert('No se pudo generar el reporte.'); + showMessage(err.message || 'No se pudo generar el reporte', 'error'); } }; @@ -189,6 +173,31 @@ export default function Reports() { const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true'; const { showMessage } = useNotification(); + const handleDownloadReport = async (reportId) => { + try { + const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-download/${reportId}/`; + const res = await fetchWithAuth(url); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } + const blob = await res.blob(); + let filename = `reporte_${reportId}.csv`; + const disposition = res.headers.get('Content-Disposition'); + if (disposition && disposition.includes('filename=')) { + filename = disposition.split('filename=')[1].replace(/"/g, '').trim(); + } + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch (err) { + showMessage(err.message || 'No se pudo descargar el reporte', 'error'); + } + }; + const [isExporting, setIsExporting] = useState(false); const [exportFormat, setExportFormat] = useState('excel'); const [showExportSuccess, setShowExportSuccess] = useState(false); diff --git a/src/pages/TableroAlmacenamiento.jsx b/src/pages/TableroAlmacenamiento.jsx index 83c2f8b..f756304 100644 --- a/src/pages/TableroAlmacenamiento.jsx +++ b/src/pages/TableroAlmacenamiento.jsx @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; import fetchWithAuth from '../fetchWithAuth'; +import { useNotification } from '../context/NotificationContext'; +import { extractApiError } from '../api/apiError'; const initialFilters = { pedimento_app: '', aduana: '', @@ -12,6 +14,7 @@ const initialFilters = { contribuyente__rfc: '', }; export default function TableroAlmacenamiento() { + const { showMessage } = useNotification(); const [filters, setFilters] = useState(initialFilters); const [summary, setSummary] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -25,10 +28,13 @@ export default function TableroAlmacenamiento() { .join('&'); const url = `${import.meta.env.VITE_EFC_API_URL}/reports/table-summary/${params ? `?${params}` : ''}`; const res = await fetchWithAuth(url, { method: 'POST' }); - if (!res.ok) throw new Error('Error al generar el reporte'); - alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.'); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } + showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success'); } catch (err) { - alert('No se pudo generar el reporte.'); + showMessage(err.message || 'No se pudo generar el reporte', 'error'); } }; @@ -36,7 +42,10 @@ export default function TableroAlmacenamiento() { try { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-download/${reportId}/`; const res = await fetchWithAuth(url); - if (!res.ok) throw new Error('Error al descargar el reporte'); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } const blob = await res.blob(); let filename = `reporte_${reportId}.csv`; const disposition = res.headers.get('Content-Disposition'); @@ -50,7 +59,7 @@ export default function TableroAlmacenamiento() { link.click(); document.body.removeChild(link); } catch (err) { - alert('No se pudo descargar el reporte.'); + showMessage(err.message || 'No se pudo descargar el reporte', 'error'); } }; @@ -64,9 +73,14 @@ export default function TableroAlmacenamiento() { .join('&'); const url = `${import.meta.env.VITE_EFC_API_URL}/reports/dashboard/summary/${params ? `?${params}` : ''}`; const res = await fetchWithAuth(url); + if (!res.ok) { + const errMsg = await extractApiError(res); + throw new Error(errMsg); + } const data = await res.json(); setSummary(data); } catch (err) { + showMessage(err.message || 'Error al cargar el resumen', 'error'); setSummary(null); } setIsLoading(false); @@ -82,6 +96,7 @@ export default function TableroAlmacenamiento() { const data = await res.json(); setReports(data); } catch (err) { + showMessage(err.message || 'Error al cargar el historial de reportes', 'error'); setReports([]); } }; @@ -147,7 +162,7 @@ export default function TableroAlmacenamiento() { diff --git a/src/pages/UserForm.jsx b/src/pages/UserForm.jsx index 15724e5..8d43fa5 100644 --- a/src/pages/UserForm.jsx +++ b/src/pages/UserForm.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { createUser, updateUser } from '../api/users.ts'; +import { fetchRoles, fetchUserRoles, assignUserRole, revokeUserRole } from '../api/rbac'; import { useNotification } from '../context/NotificationContext'; const initialForm = { @@ -12,38 +13,31 @@ const initialForm = { confirmPassword: '', rfc: [], userType: 'agente', // 'agente' | 'importador' - groups: [], is_active: true, }; -// Perfiles disponibles en el sistema -const AVAILABLE_GROUPS = [ - { id: 1, label: 'Admin', description: 'Administrador del sistema' }, - { id: 2, label: 'Developer', description: 'Desarrollador' }, - { id: 3, label: 'User', description: 'Acceso base (requerido)' }, - { id: 4, label: 'Agente Aduanal', description: 'Agente aduanal' }, - { id: 5, label: 'Importador', description: 'Importador general' } -]; - export default function UserForm() { - const { id } = useParams(); // presente si es edición + const { id } = useParams(); const isEditing = Boolean(id); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { showMessage } = useNotification(); - // Preseleccionar tipo desde query param (?type=agente|importador) const initialType = searchParams.get('type') === 'importador' ? 'importador' : 'agente'; - const [form, setForm] = useState({ - ...initialForm, - userType: initialType, - groups: initialType === 'importador' ? [3, 5] : [4, 3], - }); + const [form, setForm] = useState({ ...initialForm, userType: initialType }); const [importadores, setImportadores] = useState([]); const [submitting, setSubmitting] = useState(false); const [loadingUser, setLoadingUser] = useState(isEditing); + // RBAC: roles disponibles de la organización + const [availableRoles, setAvailableRoles] = useState([]); + const [loadingRoles, setLoadingRoles] = useState(true); + // IDs de roles seleccionados en el formulario + const [selectedRoleIds, setSelectedRoleIds] = useState([]); + // Asignaciones actuales del usuario (para calcular diff en edición): [{ id, role }] + const [currentUserRoles, setCurrentUserRoles] = useState([]); + // Validación de contraseña const [passwordValidation, setPasswordValidation] = useState({ length: false, uppercase: false, lowercase: false, number: false, special: false, @@ -75,6 +69,24 @@ export default function UserForm() { } }, []); + // Cargar roles disponibles de la organización + useEffect(() => { + fetchRoles() + .then(data => { + const list = Array.isArray(data) ? data : (data?.results ?? []); + setAvailableRoles(list); + // Preselección por tipo inicial solo en creación + if (!isEditing) { + setSelectedRoleIds(getDefaultRoleIds(list, initialType)); + } + }) + .catch(err => { + showMessage(err.message || 'Error al cargar los perfiles disponibles', 'error'); + setAvailableRoles([]); + }) + .finally(() => setLoadingRoles(false)); + }, []); + // Cargar importadores useEffect(() => { const access = localStorage.getItem('access'); @@ -82,22 +94,30 @@ export default function UserForm() { fetch(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { headers: { Authorization: `Bearer ${access}` }, }) - .then(r => r.json()) + .then(r => { + if (!r.ok) throw new Error(`Error ${r.status} al cargar importadores`); + return r.json(); + }) .then(data => setImportadores(Array.isArray(data) ? data : [])) - .catch(() => setImportadores([])); + .catch(err => { + showMessage(err.message || 'Error al cargar el catálogo de importadores', 'error'); + setImportadores([]); + }); }, []); // Cargar datos del usuario si es edición useEffect(() => { if (!isEditing) return; const access = localStorage.getItem('access'); - fetch(`${import.meta.env.VITE_EFC_API_URL}/user/users/${id}/`, { - headers: { Authorization: `Bearer ${access}` }, - }) - .then(r => r.json()) - .then(data => { - const isImportador = data.is_importador === true || - (Array.isArray(data.groups) && data.groups.includes(5)); + + Promise.all([ + fetch(`${import.meta.env.VITE_EFC_API_URL}/user/users/${id}/`, { + headers: { Authorization: `Bearer ${access}` }, + }).then(r => r.json()), + fetchUserRoles(id).catch(() => []), + ]) + .then(([data, userRoles]) => { + const isImportador = data.is_importador === true; setForm({ username: data.username || '', email: data.email || '', @@ -105,12 +125,21 @@ export default function UserForm() { last_name: data.last_name || '', password: '', confirmPassword: '', - // rfc es M2M: viene como array de PKs (strings de RFC) rfc: Array.isArray(data.rfc) ? data.rfc : (data.rfc ? [data.rfc] : []), userType: isImportador ? 'importador' : 'agente', - groups: Array.isArray(data.groups) ? data.groups : [], is_active: data.is_active !== false, }); + + // user-roles respuesta: [{ id, user: {...}, role: { id, nombre, ... }, created_at }] + const normalized = (Array.isArray(userRoles) ? userRoles : (userRoles?.results ?? [])) + .map(ur => ({ + id: ur.id, + role: typeof ur.role === 'object' ? ur.role?.id : ur.role, + })) + .filter(ur => ur.role); + + setCurrentUserRoles(normalized); + setSelectedRoleIds(normalized.map(ur => ur.role)); setLoadingUser(false); }) .catch(() => { @@ -119,6 +148,15 @@ export default function UserForm() { }); }, [id, isEditing, showMessage]); + // Preseleccionar roles por defecto según tipo de usuario + function getDefaultRoleIds(roles, type) { + const keyword = type === 'importador' ? 'importador' : 'agente'; + const matches = roles + .filter(r => r.nombre?.toLowerCase().includes(keyword)) + .map(r => r.id); + return matches; + } + const validatePassword = (password) => { const v = { length: password.length >= 8, @@ -143,7 +181,6 @@ export default function UserForm() { return isPasswordValid() && passwordsMatch && form.password.length > 0 && form.confirmPassword.length > 0; } - // En edición la contraseña es opcional if (form.password.length > 0) { return isPasswordValid() && passwordsMatch && form.confirmPassword.length > 0; } @@ -164,20 +201,18 @@ export default function UserForm() { setForm(prev => ({ ...prev, userType: type, - // Limpiar RFCs si cambia a agente rfc: type === 'agente' ? [] : prev.rfc, - // Preseleccionar perfiles por defecto según tipo - groups: type === 'importador' ? [3, 5] : [4, 3], })); + // Preseleccionar roles según tipo + setSelectedRoleIds(getDefaultRoleIds(availableRoles, type)); }; - const handleGroupToggle = (groupId) => { - setForm(prev => { - const groups = prev.groups.includes(groupId) - ? prev.groups.filter(g => g !== groupId) - : [...prev.groups, groupId]; - return { ...prev, groups }; - }); + const handleRoleToggle = (roleId) => { + setSelectedRoleIds(prev => + prev.includes(roleId) + ? prev.filter(id => id !== roleId) + : [...prev, roleId] + ); }; const handleRfcToggle = (rfc) => { @@ -190,6 +225,18 @@ export default function UserForm() { }); }; + // Sincronizar roles: calcular diff y llamar assign/revoke + const syncRoles = async (userId) => { + const currentRoleIds = currentUserRoles.map(ur => ur.role); + const toAdd = selectedRoleIds.filter(id => !currentRoleIds.includes(id)); + const toRemove = currentUserRoles.filter(ur => !selectedRoleIds.includes(ur.role)); + + await Promise.all([ + ...toAdd.map(roleId => assignUserRole(userId, roleId).catch(() => null)), + ...toRemove.map(ur => revokeUserRole(ur.id).catch(() => null)), + ]); + }; + const handleSubmit = async (e) => { e.preventDefault(); if (!isFormValid()) return; @@ -200,7 +247,6 @@ export default function UserForm() { email: form.email, first_name: form.first_name, last_name: form.last_name, - groups: form.groups, is_importador: form.userType === 'importador', is_active: form.is_active, }; @@ -211,9 +257,16 @@ export default function UserForm() { if (isEditing) { await updateUser(id, payload); + await syncRoles(id); showMessage('Usuario actualizado exitosamente', 'success'); } else { - await createUser(payload); + const newUser = await createUser(payload); + const newUserId = newUser?.id; + if (newUserId && selectedRoleIds.length > 0) { + await Promise.all( + selectedRoleIds.map(roleId => assignUserRole(newUserId, roleId).catch(() => null)) + ); + } showMessage('Usuario creado exitosamente', 'success'); } navigate('/users'); @@ -229,7 +282,7 @@ export default function UserForm() { if (loadingUser) { return (
-
+
); } @@ -253,7 +306,6 @@ export default function UserForm() { {isEditing ? 'Modifica los datos del usuario seleccionado' : 'Registro en el Sistema de Gestión de Usuarios'}

- {/* Botón regresar */}
- {/* Formulario */}
{/* Tipo de usuario — solo en creación */} @@ -426,7 +477,6 @@ export default function UserForm() {

No hay importadores disponibles en el catálogo.

) : (
- {/* Columna izquierda — disponibles */}
@@ -518,54 +568,91 @@ export default function UserForm() {
-
)} )} - {/* Perfiles */} + {/* Perfiles RBAC */}
-
-
- - +
+
+
+ + + +
+
+

Perfiles

+

+ Asigna los perfiles que tendrá este usuario en la organización +

+
+
+ {selectedRoleIds.length > 0 && ( + + {selectedRoleIds.length} seleccionado{selectedRoleIds.length !== 1 ? 's' : ''} + + )} +
+ + {loadingRoles ? ( +
+
+
+ ) : availableRoles.length === 0 ? ( +
+ + +

No hay perfiles disponibles en la organización

-
-

Perfiles

-

Asigna los perfiles a los que pertenecerá el usuario

-
-
-
- {AVAILABLE_GROUPS.map(group => { - const active = form.groups.includes(group.id); - return ( - - ); - })} -
+ {role.is_admin_role && ( + Administrador + )} + {active && ( +
+ + + +
+ )} + + ); + })} +
+ )}
{/* Credenciales */} @@ -585,7 +672,7 @@ export default function UserForm() {
- {/* Estado del usuario */} + {/* Estado */}
@@ -649,8 +736,6 @@ export default function UserForm() { )}
- - {/* Indicadores de validación */} {showPasswordValidation && (
@@ -736,7 +821,7 @@ export default function UserForm() {
- {/* Botones de acción */} + {/* Botones */}
diff --git a/src/utils/downloadUtils.js b/src/utils/downloadUtils.js index 6fdcd8d..dc3e483 100644 --- a/src/utils/downloadUtils.js +++ b/src/utils/downloadUtils.js @@ -1,4 +1,5 @@ import { fetchWithAuth } from '../fetchWithAuth'; +import { extractApiError } from '../api/apiError'; const API_URL = import.meta.env.VITE_EFC_API_URL; @@ -13,7 +14,8 @@ export const downloadFile = async (id, filename = 'archivo', showMessage) => { const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); if (!res.ok) { - showMessage('Error en la descarga del archivo', 'error'); + const errMsg = await extractApiError(res); + showMessage(errMsg, 'error'); return; } @@ -50,8 +52,11 @@ export const downloadBulkZip = async (ids, showMessage, pedimentoName) => { body: JSON.stringify({ document_ids: ids }) }); - if (!response.ok) throw new Error('Error en la descarga'); - + if (!response.ok) { + const errMsg = await extractApiError(response); + throw new Error(errMsg); + } + const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a');