diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..1914b16 --- /dev/null +++ b/.env.development @@ -0,0 +1,19 @@ +# Google Maps Configuration +VITE_GOOGLE_MAPS_API_KEY=AIzaSyA8G_comPVj6E68kfw6jjyJH8mZmiNI7s8 + +# API Configuration +VITE_API_BASE_URL=http://localhost:8000 +VITE_API_TIMEOUT=30000 + +# API Endpoints +VITE_USER_ENDPOINT_PATH=/api/v1/users +VITE_PORTS_ENDPOINT_PATH=/api/v1/ports +VITE_PORT_CONNECTIONS_ENDPOINT_PATH=/api/v1/port-connections +VITE_ROUTES_ENDPOINT_PATH=/api/v1/routes +VITE_EXPORTS_ENDPOINT_PATH=/api/v1/exports +VITE_ALGORITHMS_ENDPOINT_PATH=/api/v1/algorithms +VITE_OPTIMIZATION_ENDPOINT_PATH=/api/v1/routes/compute + +# Application Settings +VITE_APP_NAME=BerrySend +VITE_APP_VERSION=1.0.0 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..1e155fb --- /dev/null +++ b/.env.production @@ -0,0 +1,20 @@ +# Google Maps Configuration +VITE_GOOGLE_MAPS_API_KEY=AIzaSyDWlqL8kfdt5AwSYU1tXzPJM3NTXTkEuUM + +# API Configuration +VITE_API_BASE_URL=https://fake-api-4n0p.onrender.com +VITE_API_TIMEOUT=30000 + +# API Endpoints +VITE_USER_ENDPOINT_PATH=/api/v1/users +VITE_PORTS_ENDPOINT_PATH=/api/v1/ports +VITE_PORT_CONNECTIONS_ENDPOINT_PATH=/api/v1/port-connections +VITE_ROUTES_ENDPOINT_PATH=/api/v1/routes +VITE_EXPORTS_ENDPOINT_PATH=/api/v1/exports +VITE_ALGORITHMS_ENDPOINT_PATH=/api/v1/algorithms +VITE_OPTIMIZATION_ENDPOINT_PATH=/api/v1/routes/compute + +# Application Settings +VITE_APP_NAME=BerrySend +VITE_APP_VERSION=1.0.0 + diff --git a/.firebase/hosting.ZGlzdA.cache b/.firebase/hosting.ZGlzdA.cache new file mode 100644 index 0000000..6c97d73 --- /dev/null +++ b/.firebase/hosting.ZGlzdA.cache @@ -0,0 +1,14 @@ +index.html,1763958181407,0e3f1452c1209fb8c1e90faf9b6c3709bccffa623982d6eed47cb347f582eb3c +assets/visualization-page-qcryGJmo.css,1763958181406,8ff4683adad504937d2350f56ba3ee0fd52f06fce090c1aad940ed8b9be81066 +assets/port-page-CQhjS9DO.css,1763958181406,d39e26e9bc98df07137800aaa3000a3ad1e9ca3d2ac0e283d18b5ec572088a26 +assets/port-page.component-C6ddnWAC.js,1763958181407,f3cc824aba69a9e17b72cc535858c4be6c3ae99b01611e74e1ca35f169044a7d +assets/optimization-page-JblpntI1.css,1763958181406,7fa970e03835678644d840c533c2476557319dcf24101fd8cd4e419af2660006 +favicon.ico,1762229037809,3d3d5c038cc96d71c43ffff92b9bf099f92adf9d867d0849ddcc2086c63c907b +assets/visualization-page.component-CmK1zWoj.js,1763958181407,1de1713c5b41f0a3c03da0d33a79200a33e2fd900f5eea0242c9736b9bdd37b4 +assets/optimization-page.component-DUNAYf-F.js,1763958181407,af16d65b8cf07c78709d8c71d1d35b923d8850bd6b5b79c29b83909505fc21e1 +assets/register-page.component-CcraQaLq.js,1763958181407,a728313ff3c4c5766db0d0361d7a32b97258bc27db5c77f5b4ca1284eb0eecdb +assets/login-page.component-B0bs8AnG.js,1763958181407,5e41d404955dfdf2ae1bcfb59baab7845cef75a36d20ca64c0b6711f16410837 +assets/auth-layout-BLnr3ucQ.css,1763958181406,eafccf5f100313bdc38848105396d8df3ad29e2c7f33dba30a0576005c84ebdc +assets/auth-layout.component-DMnwoECF.js,1763958181407,228ffdff2cf00305fec3db564da9671a3c6d1492b0a2fca8baefc8152aff6f93 +assets/index-C4Hm7WUM.css,1763958181406,a31df13b16f6b10cf19ad35c87f145ce0d560b230248af3c30ae76fa02aef28f +assets/index-DApqfwZF.js,1763958181407,30e74153ac3146b4256b7b2b86a0328423f74d29ac4cc77c110cd1b9502b384d diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..9986aaa --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "berrysend-app" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..418fbcc --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +on: + push: + branches: + - develop +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_BERRYSEND_APP }} + channelId: live + projectId: berrysend-app diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..64161f6 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,21 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: pull_request +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_BERRYSEND_APP }} + projectId: berrysend-app diff --git a/API_URL_CHANGE.md b/API_URL_CHANGE.md new file mode 100644 index 0000000..d3b6b4c --- /dev/null +++ b/API_URL_CHANGE.md @@ -0,0 +1,135 @@ +# ✅ Cambio de API Completado + +## 🔄 Lo Que Se Cambió + +Se ha actualizado la URL de la API de `localhost:3000` a la API en la nube: + +``` +❌ ANTES: http://localhost:3000 +✅ AHORA: https://fake-api-4n0p.onrender.com +``` + +## 📝 Archivos Actualizados + +### `.env.development` +```env +# Antes +VITE_API_BASE_URL=http://localhost:3000 + +# Ahora +VITE_API_BASE_URL=https://fake-api-4n0p.onrender.com +``` + +## 🚀 Próximos Pasos + +### 1. Reinicia el servidor +```bash +npm run dev +``` + +### 2. Verifica la conexión +- Abre la consola (F12) +- Ejecuta: +```javascript +quickConfigCheck() +``` + +**Deberías ver:** +``` +🔧 BerrySend Config Check +API: https://fake-api-4n0p.onrender.com +Ports: https://fake-api-4n0p.onrender.com/api/v1/ports +Exports: https://fake-api-4n0p.onrender.com/exports +✅ Config OK +``` + +## ⚙️ Cómo Funciona + +El sistema ahora usa esta estructura: + +``` +1. Tu navegador + ↓ +2. Aplicación BerrySend (http://localhost:5173) + ↓ +3. API en la nube (https://fake-api-4n0p.onrender.com) + ↓ +4. Base de datos +``` + +## 🔐 Cambios en Diferentes Ambientes + +### Desarrollo (Development) +```env +VITE_API_BASE_URL=https://fake-api-4n0p.onrender.com +``` + +### Producción (Production) - OPCIONAL +Si necesitas cambiar también la producción, edita `.env.production` + +## ✨ Ventajas + +✅ **API en la nube** - No necesitas servidor local +✅ **Mismo endpoint** para todos +✅ **Escalable** - La API puede crecer sin afectar tu código +✅ **HTTPS** - Conexión segura + +## 🔍 Verificación + +Después de reiniciar el servidor, verifica: + +1. **Conectividad**: Los datos deben cargar sin errores +2. **Consola**: No debe haber errores de CORS o conexión +3. **Funcionalidad**: Todas las páginas deben funcionar normalmente + +## 📊 Endpoints Disponibles + +Tu aplicación usará estos endpoints: + +``` +✅ https://fake-api-4n0p.onrender.com/api/v1/ports +✅ https://fake-api-4n0p.onrender.com/api/v1/port-connections +✅ https://fake-api-4n0p.onrender.com/exports +✅ https://fake-api-4n0p.onrender.com/algorithms +✅ https://fake-api-4n0p.onrender.com/api/v1/routes/compute +``` + +## ⚡ Cambios Rápidos + +### Si necesitas volver a localhost +```env +VITE_API_BASE_URL=http://localhost:3000 +``` + +### Si necesitas otra URL +```env +VITE_API_BASE_URL=https://tu-api-url.com +``` + +Solo edita `.env.development` y reinicia el servidor. + +## 📞 Troubleshooting + +### Problema: CORS Error +**Solución:** La API en la nube debe permitir CORS desde tu dominio + +### Problema: Timeout +**Solución:** Aumenta el timeout en `.env.development`: +```env +VITE_API_TIMEOUT=60000 +``` + +### Problema: Datos no cargan +**Solución:** Verifica que la URL sea correcta: +```javascript +// En consola +import.meta.env.VITE_API_BASE_URL +// Debe mostrar: https://fake-api-4n0p.onrender.com +``` + +--- + +**Estado:** ✅ COMPLETADO +**Fecha:** 23 de Noviembre, 2025 +**API:** https://fake-api-4n0p.onrender.com + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/src/auth/services/auth.service.js b/src/auth/services/auth.service.js index 166c33c..a8fafe4 100644 --- a/src/auth/services/auth.service.js +++ b/src/auth/services/auth.service.js @@ -6,9 +6,10 @@ * @module modules/auth/services/authService */ +import axios from 'axios'; import {User} from "@/auth/model/user.entity.js"; import {AuthToken} from "@/auth/model/auth-token.entity.js"; -import userService from "@/auth/services/user.service.js"; +import { buildApiUrl, getApiTimeout } from '@/config/environment.js'; /** * Auth Service class for API interactions @@ -24,22 +25,29 @@ class AuthService { */ async login(email, password) { try { - // Use userService to find user directly from db.json - const user = await userService.findByEmailAndPassword(email, password); - - if (!user) { - throw new Error('Invalid email or password'); - } + const response = await axios.post( + `${buildApiUrl('auth')}/sign-in`, + { email, password }, + { timeout: getApiTimeout() } + ); - // Generate mock token + const data = response.data; + + // Backend response: { id, full_name, email, token } const token = AuthToken.fromAPI({ - accessToken: `mock_token_${Date.now()}`, - refreshToken: `mock_refresh_${Date.now()}`, - expiresIn: 3600, + accessToken: data.token, + refreshToken: '', + expiresIn: 1800, // 30 minutes tokenType: 'Bearer', createdAt: new Date().toISOString() }); + const user = User.fromAPI({ + id: data.id, + name: data.full_name, + email: data.email + }); + return { token, user @@ -47,10 +55,12 @@ class AuthService { } catch (error) { console.error('Error during login:', error); - if (error.message === 'Invalid email or password') { - throw error; + if (error.response?.status === 400) { + throw new Error('Invalid email or password'); } - throw new Error('Failed to login. Please try again.'); + + const errorMessage = error.response?.data?.detail || 'Failed to login. Please try again.'; + throw new Error(errorMessage); } } @@ -67,34 +77,44 @@ class AuthService { */ async register(userData) { try { - // Use userService to create user directly in db.json - const user = await userService.create({ - email: userData.email, - password: userData.password, - name: userData.name, - role: 'user' - }); + const response = await axios.post( + `${buildApiUrl('auth')}/sign-up`, + { + full_name: userData.name, + email: userData.email, + password: userData.password, + confirm_password: userData.password + }, + { timeout: getApiTimeout() } + ); - // Generate mock token - const token = AuthToken.fromAPI({ - accessToken: `mock_token_${Date.now()}`, - refreshToken: `mock_refresh_${Date.now()}`, - expiresIn: 3600, - tokenType: 'Bearer', - createdAt: new Date().toISOString() + const data = response.data; + + // Backend response: { id, full_name, email } (no token in sign-up) + // User needs to login after registration + const user = User.fromAPI({ + id: data.id, + name: data.full_name, + email: data.email }); + // No token returned from sign-up, return null token return { - token, + token: null, user }; } catch (error) { console.error('Error during registration:', error); - if (error.message === 'Email already exists') { - throw error; + if (error.response?.status === 400) { + const errorDetail = error.response?.data?.detail; + if (typeof errorDetail === 'string' && errorDetail.includes('already')) { + throw new Error('Email already registered'); + } } - throw new Error('Failed to register. Please try again.'); + + const errorMessage = error.response?.data?.detail || 'Failed to register. Please try again.'; + throw new Error(errorMessage); } } diff --git a/src/config/environment.js b/src/config/environment.js index 177161e..61dd473 100644 --- a/src/config/environment.js +++ b/src/config/environment.js @@ -11,11 +11,11 @@ export const getEnvironmentConfig = () => { const config = { // API Configuration - apiBaseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000', + apiBaseUrl: import.meta.env.VITE_API_BASE_URL, apiTimeout: import.meta.env.VITE_API_TIMEOUT || 30000, // Google Maps Configuration - googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY || '', + googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY, // Application Settings appName: import.meta.env.VITE_APP_NAME || 'BerrySend', @@ -23,11 +23,13 @@ export const getEnvironmentConfig = () => { // Endpoints (can be customized via env) endpoints: { + auth: import.meta.env.VITE_AUTH_ENDPOINT_PATH || '/api/v1/auth', users: import.meta.env.VITE_USER_ENDPOINT_PATH || '/users', ports: import.meta.env.VITE_PORTS_ENDPOINT_PATH || '/api/v1/ports', portConnections: import.meta.env.VITE_PORT_CONNECTIONS_ENDPOINT_PATH || '/api/v1/port-connections', routes: import.meta.env.VITE_ROUTES_ENDPOINT_PATH || '/api/v1/routes', - exports: import.meta.env.VITE_EXPORTS_ENDPOINT_PATH || '/exports', + optimizedRoutes: import.meta.env.VITE_OPTIMIZED_ROUTES_ENDPOINT_PATH || '/api/v1/optimized-routes', + exports: import.meta.env.VITE_EXPORTS_ENDPOINT_PATH || '/api/v1/exports', algorithms: import.meta.env.VITE_ALGORITHMS_ENDPOINT_PATH || '/algorithms', optimization: import.meta.env.VITE_OPTIMIZATION_ENDPOINT_PATH || '/api/v1/routes/compute' }, @@ -69,8 +71,9 @@ export const validateEnvironment = () => { console.warn('Warning: VITE_GOOGLE_MAPS_API_KEY is not configured'); } - if (!config.apiBaseUrl) { - throw new Error('Critical: VITE_API_BASE_URL is not configured'); + // apiBaseUrl can be empty in development (uses Vite proxy) + if (config.isProduction && !config.apiBaseUrl) { + throw new Error('Critical: VITE_API_BASE_URL must be configured for production'); } }; diff --git a/src/export-management/pages/visualization-page.component.vue b/src/export-management/pages/visualization-page.component.vue index e15dc26..2033315 100644 --- a/src/export-management/pages/visualization-page.component.vue +++ b/src/export-management/pages/visualization-page.component.vue @@ -223,6 +223,112 @@

Rutas Aéreas

+ + +
+
+
+

Guardar Export

+ +
+ +

+ La ruta ha sido optimizada exitosamente. Complete la información para guardar el export. +

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+

Información de la Ruta

+
+

Modo: {{ pendingExport.transportation_mode }}

+

Peso bruto: {{ pendingExport.gross_weight }} kg

+

Peso neto: {{ pendingExport.net_weight }} kg

+

Valor FOB: ${{ pendingExport.us_fob.toLocaleString() }}

+
+
+
+ + +
+ + +
+
+
@@ -262,6 +368,16 @@ export default { const exportsList = ref([]); const loadingExports = ref(false); const selectedExport = ref(null); + + // Pending export from optimization + const pendingExport = ref(null); + const showExportModal = ref(false); + const exportForm = ref({ + comercial_description: '', + unit: 'ton', + quantity: 0 + }); + const savingExport = ref(false); const transportModes = [ { id: 'all', label: 'Etiquetas', icon: EyeIcon, activeClass: 'bg-blue-500 text-white' }, @@ -310,51 +426,16 @@ export default { const loadGraphData = async () => { try { - const [nodesRes, edgesRes, optimalRes] = await Promise.all([ + const [nodesRes, edgesRes] = await Promise.all([ graphService.getNodes(), - graphService.getEdges(), - graphService.getOptimalRoute() + graphService.getEdges() ]); graphNodes.value = nodesRes.data.map(node => new GraphNode(node)); graphEdges.value = edgesRes.data.map(e => new GraphEdge(e)); - const routeNodeIds = optimalRes.data; - const optimalRouteEdges = []; - - if (Array.isArray(routeNodeIds) && routeNodeIds.length > 0) { - if (typeof routeNodeIds[0] === 'string') { - for (let i = 0; i < routeNodeIds.length - 1; i++) { - const fromId = routeNodeIds[i]; - const toId = routeNodeIds[i + 1]; - - const edge = graphEdges.value.find(e => - (e.source === fromId && e.target === toId) || - (e.source === toId && e.target === fromId) || - (e.port_a_id === fromId && e.port_b_id === toId) || - (e.port_a_id === toId && e.port_b_id === fromId) - ); - - if (edge) { - optimalRouteEdges.push(edge); - } else { - optimalRouteEdges.push(new GraphEdge({ - source: fromId, - target: toId, - mode: 'maritime', - distance: 0, - time: 0, - cost: 0, - isOptimal: true - })); - } - } - } else { - optimalRouteEdges.push(...routeNodeIds.map(edge => new GraphEdge(edge))); - } - } - - optimalRoute.value = optimalRouteEdges; + // Optimal route will be loaded from session storage or when an export is selected + optimalRoute.value = []; } catch (error) { console.error('Error loading graph data:', error); @@ -376,56 +457,273 @@ export default { const selectExport = async (exportItem) => { selectedExport.value = exportItem; - const routeStr = exportItem.route_id; - const portIds = routeStr.split('-'); - - console.log('📍 Export seleccionado:', exportItem.commercial_description); - console.log('🛣️ Route ID:', routeStr); - console.log('🚢 Puertos en ruta:', portIds); + console.log('📍 Export seleccionado:', exportItem); try { - const [portsRes, connectionsRes] = await Promise.all([ - graphService.getNodes(), - graphService.getEdges() - ]); - - graphNodes.value = portsRes.data.map(node => new GraphNode(node)); - graphEdges.value = connectionsRes.data.map(edge => new GraphEdge(edge)); - - console.log('✅ Total de nodos:', graphNodes.value.length); - console.log('✅ Total de aristas:', graphEdges.value.length); - - const optimalRouteEdges = []; - for (let i = 0; i < portIds.length - 1; i++) { - const fromId = portIds[i]; - const toId = portIds[i + 1]; - - const edge = graphEdges.value.find(e => - (e.source === fromId && e.target === toId) || - (e.source === toId && e.target === fromId) || - (e.port_a_id === fromId && e.port_b_id === toId) || - (e.port_a_id === toId && e.port_b_id === fromId) - ); - - if (edge) { - optimalRouteEdges.push(edge); - console.log(`✅ Arista ${i}: ${fromId} → ${toId}`); - } else { - console.warn(`⚠️ No se encontró arista entre ${fromId} y ${toId}`); + // If export has optimized_route in the response, use it directly + if (exportItem.optimized_route && exportItem.optimized_route.visited_ports) { + const routeData = exportItem.optimized_route; + console.log('✅ Ruta optimizada encontrada en export:', routeData); + + // Create edges between consecutive ports using visited_ports + const routeEdges = []; + const visitedPorts = routeData.visited_ports; + + for (let i = 0; i < visitedPorts.length - 1; i++) { + const fromPortName = visitedPorts[i]; + const toPortName = visitedPorts[i + 1]; + + // Find port nodes by name + const fromNode = graphNodes.value.find(n => n.name === fromPortName); + const toNode = graphNodes.value.find(n => n.name === toPortName); + + if (!fromNode || !toNode) { + console.warn(`Port not found: ${fromPortName} or ${toPortName}`); + continue; + } + + // Try to find existing edge in graph + const existingEdge = graphEdges.value.find(e => + (e.source === fromNode.id && e.target === toNode.id) || + (e.target === fromNode.id && e.source === toNode.id) + ); + + if (existingEdge) { + routeEdges.push(new GraphEdge({ + ...existingEdge, + isOptimal: true + })); + } else { + // Create new edge for visualization + routeEdges.push(new GraphEdge({ + source: fromNode.id, + target: toNode.id, + mode: routeData.route_mode || 'maritime', + isOptimal: true + })); + } } + + optimalRoute.value = routeEdges; + console.log('🎯 Ruta óptima cargada:', routeEdges.length, 'conexiones'); + + } else if (exportItem.optimized_route_id) { + // Export has route ID but not the full route data + // Try to fetch the route separately + console.log('📡 Cargando ruta por ID:', exportItem.optimized_route_id); + + try { + const routeData = await ExportService.getRouteById(exportItem.optimized_route_id); + console.log('✅ Ruta cargada desde API:', routeData); + + // Create edges using the fetched route data + const routeEdges = []; + const visitedPorts = routeData.visited_ports || []; + + for (let i = 0; i < visitedPorts.length - 1; i++) { + const fromPortName = visitedPorts[i]; + const toPortName = visitedPorts[i + 1]; + + const fromNode = graphNodes.value.find(n => n.name === fromPortName); + const toNode = graphNodes.value.find(n => n.name === toPortName); + + if (!fromNode || !toNode) { + console.warn(`Port not found: ${fromPortName} or ${toPortName}`); + continue; + } + + const existingEdge = graphEdges.value.find(e => + (e.source === fromNode.id && e.target === toNode.id) || + (e.target === fromNode.id && e.source === toNode.id) + ); + + if (existingEdge) { + routeEdges.push(new GraphEdge({ + ...existingEdge, + isOptimal: true + })); + } else { + routeEdges.push(new GraphEdge({ + source: fromNode.id, + target: toNode.id, + mode: routeData.route_mode || 'maritime', + isOptimal: true + })); + } + } + + optimalRoute.value = routeEdges; + console.log('🎯 Ruta óptima cargada desde API:', routeEdges.length, 'conexiones'); + + } catch (routeError) { + console.error('❌ Error cargando ruta:', routeError); + optimalRoute.value = []; + } + + } else { + console.warn('Export no tiene ruta optimizada asociada'); + optimalRoute.value = []; } - optimalRoute.value = optimalRouteEdges; - console.log('🎯 Ruta óptima actualizada con', optimalRouteEdges.length, 'aristas en rojo'); - } catch (error) { console.error('❌ Error al seleccionar exportación:', error); } }; + /** + * Save export from optimization result + */ + const saveExport = async () => { + if (!pendingExport.value) return; + + if (!exportForm.value.comercial_description.trim()) { + alert('Por favor ingrese una descripción comercial'); + return; + } + + if (!pendingExport.value?.optimized_route_id) { + alert('Error: No se encontró el ID de la ruta optimizada'); + return; + } + + savingExport.value = true; + try { + // Get user ID from localStorage (berrysend_user key) + const storedUser = localStorage.getItem('berrysend_user'); + if (!storedUser) { + throw new Error('Usuario no autenticado. Por favor inicie sesión nuevamente.'); + } + + const userData = JSON.parse(storedUser); + const userId = userData.id; + + if (!userId) { + throw new Error('ID de usuario no encontrado'); + } + + // Validate all required fields + const gross_weight = Number(pendingExport.value.gross_weight); + const net_weight = Number(pendingExport.value.net_weight); + const us_fob = Number(pendingExport.value.us_fob); + const quantity = Number(exportForm.value.quantity); + + if (gross_weight <= 0 || net_weight <= 0 || quantity <= 0) { + throw new Error('Los pesos y cantidad deben ser mayores a 0'); + } + + const exportData = { + comercial_description: exportForm.value.comercial_description.trim(), + transportation_mode: pendingExport.value.transportation_mode, + us_fob: us_fob, + gross_weight: gross_weight, + net_weight: net_weight, + unit: exportForm.value.unit, + quantity: quantity, + optimized_route_id: pendingExport.value.optimized_route_id, + user_id: userId + }; + + const createdExport = await ExportService.createExport(exportData); + + // Clear pending data + sessionStorage.removeItem('pendingExportData'); + sessionStorage.removeItem('optimizedRoute'); + pendingExport.value = null; + showExportModal.value = false; + + // Reload exports and select the new one + await loadExports(); + const newExport = exportsList.value.find(e => e.export_id === createdExport.export_id); + if (newExport) { + selectExport(newExport); + } + + } catch (error) { + console.error('Error guardando export:', error); + + let errorMessage = 'Error al guardar el export'; + if (error.response?.data?.detail) { + if (typeof error.response.data.detail === 'string') { + errorMessage = error.response.data.detail; + } else if (Array.isArray(error.response.data.detail)) { + errorMessage = error.response.data.detail.map(e => e.msg || e.message).join('\n'); + } + } else if (error.message) { + errorMessage = error.message; + } + + alert(`Error al guardar:\n${errorMessage}`); + } finally { + savingExport.value = false; + } + }; + + /** + * Cancel export creation + */ + const cancelExport = () => { + sessionStorage.removeItem('pendingExportData'); + sessionStorage.removeItem('optimizedRoute'); + pendingExport.value = null; + showExportModal.value = false; + optimalRoute.value = []; + }; + onMounted(() => { loadGraphData(); loadExports(); + + // Check for pending export data from optimization + const pendingData = sessionStorage.getItem('pendingExportData'); + const optimizedRouteData = sessionStorage.getItem('optimizedRoute'); + + if (pendingData && optimizedRouteData) { + try { + pendingExport.value = JSON.parse(pendingData); + const routeResult = JSON.parse(optimizedRouteData); + + // Pre-fill form + exportForm.value.quantity = pendingExport.value.quantity || 0; + exportForm.value.unit = pendingExport.value.unit || 'ton'; + + // Show modal + showExportModal.value = true; + + // Load the optimized route on the map + console.log('📍 Cargando ruta optimizada en el mapa:', routeResult); + + // Wait for graph data to load first + setTimeout(async () => { + if (routeResult.visited_ports && graphNodes.value.length > 0) { + const routeEdges = []; + const visitedPorts = routeResult.visited_ports; + + for (let i = 0; i < visitedPorts.length - 1; i++) { + const fromPortName = visitedPorts[i]; + const toPortName = visitedPorts[i + 1]; + + const fromNode = graphNodes.value.find(n => n.name === fromPortName); + const toNode = graphNodes.value.find(n => n.name === toPortName); + + if (fromNode && toNode) { + routeEdges.push(new GraphEdge({ + source: fromNode.id, + target: toNode.id, + mode: routeResult.route_mode || 'maritime', + isOptimal: true + })); + } + } + + optimalRoute.value = routeEdges; + console.log('🎯 Ruta pendiente cargada en el mapa:', routeEdges.length, 'conexiones'); + } + }, 1000); + + } catch (error) { + console.error('Error loading pending export:', error); + } + } }); return { @@ -437,6 +735,12 @@ export default { statistics, exportsList, loadingExports, + pendingExport, + showExportModal, + exportForm, + savingExport, + saveExport, + cancelExport, selectedExport, selectExport, getStatusClass, diff --git a/src/export-management/services/export.service.js b/src/export-management/services/export.service.js index 0a8df68..f293a69 100644 --- a/src/export-management/services/export.service.js +++ b/src/export-management/services/export.service.js @@ -8,9 +8,10 @@ import { buildApiUrl, getApiTimeout } from '@/config/environment.js'; class ExportService { /** * Fetch all exports + * Requires authentication * * @returns {Promise} Array of export records - * @throws {Error} If API request fails + * @throws {Error} If API request fails or unauthorized */ async getAllExports() { try { @@ -20,14 +21,19 @@ class ExportService { return response.data; } catch (error) { console.error('Error fetching exports:', error); - throw new Error('Failed to fetch exports'); + if (error.response?.status === 401) { + throw new Error('Authentication required'); + } + const errorMessage = error.response?.data?.detail || 'Failed to fetch exports'; + throw new Error(errorMessage); } } /** * Get export by ID + * Requires authentication * - * @param {string} exportId - Export identifier + * @param {string} exportId - Export identifier (UUID) * @returns {Promise} Export record * @throws {Error} If export not found or API request fails */ @@ -39,7 +45,14 @@ class ExportService { return response.data; } catch (error) { console.error(`Error fetching export ${exportId}:`, error); - throw new Error('Export not found'); + if (error.response?.status === 401) { + throw new Error('Authentication required'); + } + if (error.response?.status === 404) { + throw new Error('Export not found'); + } + const errorMessage = error.response?.data?.detail || 'Failed to fetch export'; + throw new Error(errorMessage); } } @@ -65,7 +78,7 @@ class ExportService { /** * Get route details by ID * - * @param {string} routeId - Route identifier + * @param {string} routeId - Route identifier (UUID) * @returns {Promise} Route details * @throws {Error} If API request fails */ @@ -74,30 +87,58 @@ class ExportService { const response = await axios.get(`${buildApiUrl('routes')}/${routeId}`, { timeout: getApiTimeout() }); + // Backend returns: { optimized_route_id, visited_ports, total_distance, total_time, total_cost, algorithm_used, route_mode } return response.data; } catch (error) { console.error(`Error fetching route ${routeId}:`, error); - throw new Error('Route not found'); + if (error.response?.status === 404) { + throw new Error('Route not found'); + } + const errorMessage = error.response?.data?.detail || 'Failed to fetch route'; + throw new Error(errorMessage); } } /** * Create a new export with a route + * Requires authentication (JWT token) * * @param {Object} exportData - Export data - * @param {string} exportData.route_id - Route identifier (required) + * @param {string} exportData.comercial_description - Product description + * @param {string} exportData.transportation_mode - 'maritime' or 'air' + * @param {number} exportData.us_fob - FOB value in USD + * @param {number} exportData.gross_weight - Gross weight in KG + * @param {number} exportData.net_weight - Net weight in KG + * @param {string} exportData.unit - Unit of measure (e.g., 'kg', 'ton') + * @param {number} exportData.quantity - Quantity + * @param {string} exportData.optimized_route_id - Route identifier (UUID) + * @param {string} exportData.user_id - User identifier (UUID) * @returns {Promise} Created export with ID * @throws {Error} If validation fails or API request fails */ async createExport(exportData) { try { - const response = await axios.post(buildApiUrl('exports'), exportData, { - timeout: getApiTimeout() - }); + const response = await axios.post( + `${buildApiUrl('exports')}/`, + exportData, + { timeout: getApiTimeout() } + ); + // Backend returns: { export_id, ...exportData, created_at, optimized_route: {...} } return response.data; } catch (error) { console.error('Error creating export:', error); - throw new Error('Failed to create export'); + if (error.response?.status === 401) { + throw new Error('Authentication required'); + } + if (error.response?.status === 400) { + const errorDetail = error.response?.data?.detail; + throw new Error(errorDetail || 'Invalid export data'); + } + if (error.response?.status === 404) { + throw new Error('Route or user not found'); + } + const errorMessage = error.response?.data?.detail || 'Failed to create export'; + throw new Error(errorMessage); } } @@ -131,24 +172,31 @@ class ExportService { /** * Change the route associated with an export - * Simply update the route_id to swap routes + * Requires authentication (JWT token) * - * @param {string} exportId - Export identifier - * @param {string} routeId - New route identifier + * @param {string} exportId - Export identifier (UUID) + * @param {string} routeId - New route identifier (UUID) * @returns {Promise} Updated export with new route * @throws {Error} If API request fails */ async changeExportRoute(exportId, routeId) { try { - const response = await axios.put( - `${buildApiUrl('exports')}/${exportId}`, - { route_id: routeId }, + const response = await axios.patch( + `${buildApiUrl('exports')}/${exportId}/routes/${routeId}/assign`, + {}, { timeout: getApiTimeout() } ); return response.data; } catch (error) { console.error(`Error changing route for export ${exportId}:`, error); - throw new Error('Failed to change export route'); + if (error.response?.status === 401) { + throw new Error('Authentication required'); + } + if (error.response?.status === 404) { + throw new Error('Export or route not found'); + } + const errorMessage = error.response?.data?.detail || 'Failed to change export route'; + throw new Error(errorMessage); } } @@ -162,7 +210,11 @@ class ExportService { */ async updateExport(exportId, updateData) { try { - const response = await axios.put(`${API_BASE_URL}/exports/${exportId}`, updateData); + const response = await axios.put( + `${buildApiUrl('exports')}/${exportId}`, + updateData, + { timeout: getApiTimeout() } + ); return response.data; } catch (error) { console.error(`Error updating export ${exportId}:`, error); @@ -179,7 +231,10 @@ class ExportService { */ async deleteExport(exportId) { try { - await axios.delete(`${API_BASE_URL}/exports/${exportId}`); + await axios.delete( + `${buildApiUrl('exports')}/${exportId}`, + { timeout: getApiTimeout() } + ); } catch (error) { console.error(`Error deleting export ${exportId}:`, error); throw new Error('Failed to delete export'); diff --git a/src/export-management/services/graph.service.js b/src/export-management/services/graph.service.js index 5f4a3cf..d00c875 100644 --- a/src/export-management/services/graph.service.js +++ b/src/export-management/services/graph.service.js @@ -13,7 +13,5 @@ export const graphService = { getEdges: () => axios.get(`${API_URL}/api/v1/port-connections/`, { timeout: API_TIMEOUT }), - getRoutes: () => axios.get(`${API_URL}/api/v1/port-connections/`, { timeout: API_TIMEOUT }), - - getOptimalRoute: () => axios.get(`${API_URL}/optimalRoute`, { timeout: API_TIMEOUT }), + getRoutes: () => axios.get(`${API_URL}/api/v1/port-connections/`, { timeout: API_TIMEOUT }) }; diff --git a/src/port-management/model/port.entity.js b/src/port-management/model/port.entity.js index e38aa29..1a9a413 100644 --- a/src/port-management/model/port.entity.js +++ b/src/port-management/model/port.entity.js @@ -149,7 +149,21 @@ export class Port { * @returns {Port} New Port instance */ static fromAPI(data) { - return new Port(data); + // Map backend response to frontend model + return new Port({ + id: data.id, + name: data.name, + country: data.country, + type: data.type || 'intermediate', // maritime/air from backend + capacity: data.capacity || 0, + coordinates: { + latitude: data.latitude || 0, + longitude: data.longitude || 0 + }, + connections: data.connections || 0, + createdAt: data.createdAt || data.created_at, + updatedAt: data.updatedAt || data.updated_at + }); } /** diff --git a/src/port-management/services/port.service.js b/src/port-management/services/port.service.js index aa9796e..d8ab99a 100644 --- a/src/port-management/services/port.service.js +++ b/src/port-management/services/port.service.js @@ -14,18 +14,21 @@ import { buildApiUrl, getApiTimeout } from '@/config/environment.js'; */ class PortService { /** - * Fetches all ports from the mock API + * Fetches all ports from the API * - * Supports type or country filters + * Supports search filter by name * - * @param {Object} filters - * @param {string} filters.type - * @param {string} filters.country + * @param {Object} options + * @param {string} options.search - Search by port name * @returns {Promise>} */ - async getAllPorts(filters = {}) { + async getAllPorts(options = {}) { try { - const params = new URLSearchParams(filters); + const params = {}; + if (options.search) { + params.search = options.search; + } + const response = await axios.get(buildApiUrl('ports'), { params, timeout: getApiTimeout() @@ -33,7 +36,8 @@ class PortService { return Port.fromAPIArray(response.data); } catch (error) { console.error('Error fetching ports:', error); - throw new Error('Failed to fetch ports'); + const errorMessage = error.response?.data?.detail || 'Failed to fetch ports'; + throw new Error(errorMessage); } } @@ -51,7 +55,11 @@ class PortService { return Port.fromAPI(response.data); } catch (error) { console.error(`Error fetching port ${portId}:`, error); - throw new Error(`Failed to fetch port with ID: ${portId}`); + if (error.response?.status === 404) { + throw new Error('Port not found'); + } + const errorMessage = error.response?.data?.detail || `Failed to fetch port with ID: ${portId}`; + throw new Error(errorMessage); } } @@ -69,7 +77,11 @@ class PortService { } try { - const response = await axios.post(`${API_BASE_URL}/ports`, port.toJSON()); + const response = await axios.post( + buildApiUrl('ports'), + port.toJSON(), + { timeout: getApiTimeout() } + ); return Port.fromAPI(response.data); } catch (error) { console.error('Error creating port:', error); @@ -92,7 +104,11 @@ class PortService { } try { - const response = await axios.put(`${API_BASE_URL}/ports/${portId}`, port.toJSON()); + const response = await axios.put( + `${buildApiUrl('ports')}/${portId}`, + port.toJSON(), + { timeout: getApiTimeout() } + ); return Port.fromAPI(response.data); } catch (error) { console.error(`Error updating port ${portId}:`, error); @@ -108,7 +124,10 @@ class PortService { */ async deletePort(portId) { try { - await axios.delete(`${API_BASE_URL}/ports/${portId}`); + await axios.delete( + `${buildApiUrl('ports')}/${portId}`, + { timeout: getApiTimeout() } + ); } catch (error) { console.error(`Error deleting port ${portId}:`, error); throw new Error(`Failed to delete port with ID: ${portId}`); @@ -116,46 +135,44 @@ class PortService { } /** - * Fetches edges (connections) linked to a specific port + * Fetches connections linked to a specific port * * @param {string} portId * @returns {Promise>} */ async getPortConnections(portId) { try { - const response = await axios.get(`${API_BASE_URL}/edges`, { - params: { - source: portId, - target: portId - } + const response = await axios.get(`${buildApiUrl('ports')}/${portId}/connections`, { + timeout: getApiTimeout() }); - - const directConnections = response.data.filter( - edge => edge.source === portId || edge.target === portId - ); - - return directConnections; + return response.data; } catch (error) { console.error(`Error fetching connections for port ${portId}:`, error); - throw new Error(`Failed to fetch connections for port: ${portId}`); + if (error.response?.status === 404) { + throw new Error('Port not found'); + } + const errorMessage = error.response?.data?.detail || `Failed to fetch connections for port: ${portId}`; + throw new Error(errorMessage); } } /** - * Searches ports by name or type (json-server supports q= for full-text) + * Searches ports by name * * @param {string} query * @returns {Promise>} */ async searchPorts(query) { try { - const response = await axios.get(`${API_BASE_URL}/ports`, { - params: { q: query } + const response = await axios.get(buildApiUrl('ports'), { + params: { search: query }, + timeout: getApiTimeout() }); return Port.fromAPIArray(response.data); } catch (error) { console.error('Error searching ports:', error); - throw new Error('Failed to search ports'); + const errorMessage = error.response?.data?.detail || 'Failed to search ports'; + throw new Error(errorMessage); } } } diff --git a/src/route-optimization/model/algorithm.entity.js b/src/route-optimization/model/algorithm.entity.js index 211a3e8..cf6f5f6 100644 --- a/src/route-optimization/model/algorithm.entity.js +++ b/src/route-optimization/model/algorithm.entity.js @@ -160,7 +160,7 @@ export const PredefinedAlgorithms = { isRecommended: true }, A_STAR: { - id: 'a-star', + id: 'a*', name: 'A*', description: 'Búsqueda heurística eficiente', complexity: 'O(b^d)', diff --git a/src/route-optimization/pages/optimization-page.component.vue b/src/route-optimization/pages/optimization-page.component.vue index 655ab2f..fbf946d 100644 --- a/src/route-optimization/pages/optimization-page.component.vue +++ b/src/route-optimization/pages/optimization-page.component.vue @@ -136,6 +136,84 @@ {{ isOptimizing ? 'Optimizando Ruta...' : 'Optimizar Ruta' }} + + + + + + +
+

+ + + + Comparación de Algoritmos +

+ +
+
+
+

+ + ⭐ Mejor Opción + + {{ result.algorithm }} +

+ {{ result.execution_time }}ms +
+ +
+
+

Distancia

+

{{ result.total_distance }} km

+
+
+

Tiempo

+

{{ result.total_time }} hrs

+
+
+

Costo

+

${{ result.total_cost.toLocaleString() }}

+
+
+ +
+

Ruta:

+

{{ result.visited_ports.join(' → ') }}

+
+
+
@@ -272,6 +350,7 @@