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