From 39bd572955edef6c2e68c782e0069640fa7ac59d Mon Sep 17 00:00:00 2001 From: Diego Mireles Date: Sat, 23 May 2026 00:07:00 -0600 Subject: [PATCH] feat: add api implementation --- frontend/INTEGRATION.md | 409 ++++++++++++++++++++++ frontend/src/app/_layout.tsx | 8 + frontend/src/app/alerts.tsx | 54 ++- frontend/src/app/index.tsx | 43 ++- frontend/src/app/login.tsx | 40 +++ frontend/src/app/profile.tsx | 72 +++- frontend/src/config/api.ts | 1 + frontend/src/context/AppContext.tsx | 102 ++++++ frontend/src/lib/api.ts | 30 ++ frontend/src/lib/notification-mapper.ts | 14 + frontend/src/services/auth.service.ts | 25 ++ frontend/src/services/tracking.service.ts | 46 +++ 12 files changed, 799 insertions(+), 45 deletions(-) create mode 100644 frontend/INTEGRATION.md create mode 100644 frontend/src/app/login.tsx create mode 100644 frontend/src/config/api.ts create mode 100644 frontend/src/context/AppContext.tsx create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/notification-mapper.ts create mode 100644 frontend/src/services/auth.service.ts create mode 100644 frontend/src/services/tracking.service.ts diff --git a/frontend/INTEGRATION.md b/frontend/INTEGRATION.md new file mode 100644 index 0000000..60f364d --- /dev/null +++ b/frontend/INTEGRATION.md @@ -0,0 +1,409 @@ +# Guía Frontend — Integración con el Backend + +App: Expo Router + React Native + TypeScript. +Backend: Express en `http://localhost:3000` (ya está listo, no hay que tocarlo). + +> Esta guía está pensada para probar en **emulador de Android**. +> Si prueban en otro entorno, sólo cambien la `API_URL` (sección 1). + +--- + +## 0. Endpoints del backend + +| Método | Endpoint | Para qué | +|---|---|---| +| POST | `/api/auth/register` | Crear cuenta | +| POST | `/api/auth/login` | Iniciar sesión y obtener JWT | +| GET | `/api/auth/me` | Datos del usuario (requiere token) | +| POST | `/api/tracking/gps-update` | Recibe posición del camión, regresa ETA + notificaciones | + +--- + +## 1. Configurar la URL del backend + +Crear `frontend/src/config/api.ts`: + +```ts +// IPs según el entorno: +// Android Emulator → http://10.0.2.2:3000 (default aquí) +// iOS simulator → http://localhost:3000 +// Dispositivo real → http://:3000 (ej. 192.168.1.20) +// Expo Go web → http://localhost:3000 +export const API_URL = "http://10.0.2.2:3000"; +``` + +> `10.0.2.2` es la dirección especial del emulador de Android para alcanzar el `localhost` de la laptop. No usen `localhost` desde el emulador, no funciona. + +> Si prueban en un celular físico, sáquen la IP de la laptop con `ipconfig` (Windows) y pongan algo como `http://192.168.1.20:3000`. La laptop y el celular tienen que estar en el **mismo Wi-Fi**. + +--- + +## 2. Cliente HTTP simple + +Crear `frontend/src/lib/api.ts`: + +```ts +import { API_URL } from "../config/api"; + +let authToken: string | null = null; + +export const setAuthToken = (token: string | null) => { + authToken = token; +}; + +export const apiFetch = async ( + path: string, + options: RequestInit = {}, +): Promise => { + const headers: Record = { + "Content-Type": "application/json", + ...(options.headers as Record), + }; + + if (authToken) { + headers.Authorization = `Bearer ${authToken}`; + } + + const res = await fetch(`${API_URL}${path}`, { ...options, headers }); + const body = await res.json().catch(() => ({})); + + if (!res.ok) { + throw new Error(body.error ?? `HTTP ${res.status}`); + } + + return body as T; +}; +``` + +--- + +## 3. Servicios por feature + +### `frontend/src/services/auth.service.ts` + +```ts +import { apiFetch, setAuthToken } from "../lib/api"; + +export interface AuthUser { + id: number; + email: string; + name: string; +} + +export const register = (data: { name: string; email: string; password: string }) => + apiFetch<{ id: number; email: string; name: string }>("/api/auth/register", { + method: "POST", + body: JSON.stringify(data), + }); + +export const login = async (data: { email: string; password: string }) => { + const res = await apiFetch<{ user: AuthUser; token: string }>("/api/auth/login", { + method: "POST", + body: JSON.stringify(data), + }); + setAuthToken(res.token); + return res; +}; + +export const getMe = () => + apiFetch("/api/auth/me"); +``` + +### `frontend/src/services/tracking.service.ts` + +```ts +import { apiFetch } from "../lib/api"; + +export type NotificationType = + | "ROUTE_START" + | "TRUCK_PROXIMITY" + | "ROUTE_COMPLETED" + | "DELAY" + | "MECHANICAL_FAILURE"; + +export interface GpsUpdatePayload { + truckId: string; + routeId: string; + lat: number; + lng: number; + speed: number; + status: "EN_RUTA" | "DETENIDO" | "FINALIZADO" | "FALLA"; + positionId?: number; + timestamp?: string; +} + +export interface EtaResult { + etaMinutes: number; + arrivalWindow: { from: string; to: string }; + message: string; +} + +export interface BackendNotification { + userId: number; + type: NotificationType; + title: string; + body: string; +} + +export interface GpsUpdateResponse { + message: string; + truck: { truckId: number; routeId: string; status: string }; + eta: EtaResult; + notifications: BackendNotification[]; +} + +export const sendGpsUpdate = (payload: GpsUpdatePayload) => + apiFetch("/api/tracking/gps-update", { + method: "POST", + body: JSON.stringify(payload), + }); +``` + +--- + +## 4. Mapear los tipos del backend al componente `AlertItem` + +`AlertItem` ya acepta `started | near | danger | completed`. Crear `frontend/src/lib/notification-mapper.ts`: + +```ts +import type { NotificationType } from "../services/tracking.service"; + +export const notificationTypeToAlertType = ( + type: NotificationType, +): "started" | "near" | "danger" | "completed" => { + switch (type) { + case "ROUTE_START": return "started"; + case "TRUCK_PROXIMITY": return "near"; + case "ROUTE_COMPLETED": return "completed"; + case "DELAY": return "danger"; + case "MECHANICAL_FAILURE": return "danger"; + } +}; +``` + +--- + +## 5. Pantalla de Login (`src/app/login.tsx`) + +```tsx +import { useState } from "react"; +import { View, Text, StyleSheet, Alert } from "react-native"; +import { useRouter } from "expo-router"; +import InputField from "../components/InputField"; +import PrimaryButton from "../components/PrimaryButton"; +import { login } from "../services/auth.service"; + +export default function LoginScreen() { + const [email, setEmail] = useState("ana@test.com"); + const [password, setPassword] = useState("123456"); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const handleLogin = async () => { + try { + setLoading(true); + await login({ email, password }); + router.replace("/"); + } catch (err) { + Alert.alert("Error", err instanceof Error ? err.message : "Login failed"); + } finally { + setLoading(false); + } + }; + + return ( + + Iniciar sesión + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, padding: 24, justifyContent: "center" }, + title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 }, +}); +``` + +--- + +## 6. Pantalla principal con ETA en tiempo real (`src/app/index.tsx`) + +```tsx +import { useEffect, useState } from "react"; +import { ScrollView, RefreshControl, View } from "react-native"; +import EtaCard from "../components/EtaCard"; +import { + sendGpsUpdate, + type GpsUpdateResponse, + type BackendNotification, +} from "../services/tracking.service"; + +// Para la demo: el frontend simula el avance del camión. +// En producción esto vendría de un GPS real → backend → app vía websockets/polling. +const SIMULATION_STEPS = [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:00:00Z" }, + { positionId: 2, lat: 20.5185, lng: -100.8450, speed: 45, timestamp: "2026-05-22T06:12:00Z" }, + { positionId: 3, lat: 20.5215, lng: -100.8142, speed: 22, timestamp: "2026-05-22T06:25:00Z" }, + { positionId: 4, lat: 20.5212, lng: -100.8175, speed: 15, timestamp: "2026-05-22T06:38:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:40:00Z" }, +]; + +export default function HomeScreen() { + const [stepIndex, setStepIndex] = useState(0); + const [data, setData] = useState(null); + const [notifications, setNotifications] = useState([]); + + const fetchNext = async () => { + const step = SIMULATION_STEPS[stepIndex % SIMULATION_STEPS.length]; + try { + const res = await sendGpsUpdate({ + truckId: "101", + routeId: "RUTA-01", + status: "EN_RUTA", + ...step, + }); + setData(res); + setNotifications((prev) => [...res.notifications, ...prev]); + setStepIndex((i) => i + 1); + } catch (err) { + console.error(err); + } + }; + + useEffect(() => { fetchNext(); }, []); + + return ( + }> + + + {/* Mapear notifications a aquí si quieren mostrarlas en la home también */} + + + ); +} +``` + +> **Truco del demo:** cada "pull to refresh" avanza el camión un paso. Sin tocar el backend, pueden mostrar las 5 fases del recorrido. + +--- + +## 7. Pantalla de Alertas (`src/app/alerts.tsx`) + +```tsx +import { FlatList, View } from "react-native"; +import AlertItem from "../components/Alertltem"; +import { notificationTypeToAlertType } from "../lib/notification-mapper"; +import type { BackendNotification } from "../services/tracking.service"; + +type Props = { notifications: BackendNotification[] }; + +export default function AlertsScreen({ notifications }: Props) { + return ( + + String(i)} + renderItem={({ item }) => ( + + )} + /> + + ); +} +``` + +> **Compartir notificaciones entre pantallas:** lo más simple para el demo es subirlas a un `Context` o Zustand. Si tienen tiempo. Para hackathon, pueden duplicar la lógica o pasarlas por params. + +--- + +## 8. Cómo probar el flujo completo en el emulador de Android + +### Pre-requisitos + +- Backend corriendo: `npm run dev` dentro de `/backend`. Debe imprimir `Server is running on port 3000`. +- Android Studio con AVD (emulador) abierto. +- Frontend con dependencias instaladas: `npm install` dentro de `/frontend`. + +### Pasos + +1. **Levantar el frontend:** + + ```powershell + cd frontend + npm run android + ``` + + Eso abre Expo y lanza la app en el emulador. + +2. **Crear un usuario de prueba** (sólo la primera vez). Desde Postman: + + ``` + POST http://localhost:3000/api/auth/register + Content-Type: application/json + + { "name": "Ana López", "email": "ana@test.com", "password": "123456" } + ``` + + (Ana ya está en el mock con `routeId: "RUTA-01"`, así que recibirá las notificaciones del camión 101.) + +3. **En la app del emulador**, iniciar sesión con: + - Email: `ana@test.com` + - Password: `123456` + +4. **En la pantalla principal**, hacer **pull-to-refresh** repetidamente. Cada pull avanza el camión: + + | Paso | positionId | Notificación esperada | + |---|---|---| + | 1 | 1 | (ninguna) | + | 2 | 2 | "¡Ruta Iniciada!" | + | 3 | 3 | (ninguna) | + | 4 | 4 | "Camión Cercano" | + | 5 | 8 | "Servicio Finalizado" | + +5. **Pantalla de alertas** debe mostrar las notificaciones acumuladas con los iconos y colores correctos. + +--- + +## 9. Problemas comunes en Android emulator + +| Síntoma | Causa | Solución | +|---|---|---| +| `Network request failed` | Apunta a `localhost` | Usar `http://10.0.2.2:3000` en `API_URL` | +| `TypeError: Failed to fetch` (en web) | Falta CORS | Avisar al del backend para que active `cors()` | +| 401 al hacer login | Usuario no existe | Registrar primero con Postman (sección 8, paso 2) | +| `notifications: []` aunque cambies positionId | El cache ya envió esa noti antes | Reiniciar el backend (`Ctrl+C` y `npm run dev`) | +| ETA negativo o raro | El `timestamp` que envías está atrás del target | Es normal, el mensaje sale como "El camión ya pasó..." | + +--- + +## 10. Checklist mínimo + +- [ ] `src/config/api.ts` con `http://10.0.2.2:3000` +- [ ] `src/lib/api.ts` (fetch wrapper) +- [ ] `src/services/auth.service.ts` +- [ ] `src/services/tracking.service.ts` +- [ ] `src/lib/notification-mapper.ts` +- [ ] Pantalla de login funcionando con `ana@test.com` +- [ ] Pantalla principal mostrando `EtaCard` con datos reales +- [ ] Pantalla de alertas mostrando `AlertItem` con notificaciones reales +- [ ] Probar las 5 fases del recorrido con pull-to-refresh + +--- + +## 11. Lo que NO necesitan hacer + +- ❌ Push notifications reales (Firebase / Expo Notifications) — sólo lista in-app +- ❌ Mapas con GPS real — los datos vienen del mock del backend +- ❌ AsyncStorage para el token — para el demo basta con estado en memoria +- ❌ Modificar nada del `/backend` diff --git a/frontend/src/app/_layout.tsx b/frontend/src/app/_layout.tsx index f354518..85558d1 100644 --- a/frontend/src/app/_layout.tsx +++ b/frontend/src/app/_layout.tsx @@ -1,8 +1,10 @@ import { Tabs } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; +import { AppProvider } from "../context/AppContext"; export default function Layout() { return ( + + + + ); } diff --git a/frontend/src/app/alerts.tsx b/frontend/src/app/alerts.tsx index 9115065..d5262a6 100644 --- a/frontend/src/app/alerts.tsx +++ b/frontend/src/app/alerts.tsx @@ -1,12 +1,22 @@ import { ScrollView, Text } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { Redirect } from "expo-router"; import { COLORS } from "../constants/colors"; import SectionTitle from "../components/SectionTitle"; import AlertItem from "@/components/Alertltem"; +import { useApp } from "../context/AppContext"; +import { notificationTypeToAlertType } from "../lib/notification-mapper"; + export default function AlertsScreen() { + const { user, notifications } = useApp(); + + if (!user) { + return ; + } + return ( - Estado actual de recolección + > + {notifications.length > 0 + ? `${notifications.length} notificaciones` + : "Aún no hay alertas. Vuelve al inicio y desliza para actualizar."} - - - - - - - + {notifications.map((n, i) => ( + + ))} ); -} \ No newline at end of file +} diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 7aef6a6..c7469e5 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -1,5 +1,7 @@ -import { ScrollView, View, Text } from "react-native"; +import { useEffect } from "react"; +import { ScrollView, View, Text, RefreshControl } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { Redirect, useRouter } from "expo-router"; import { COLORS } from "../constants/colors"; @@ -7,7 +9,27 @@ import SectionTitle from "../components/SectionTitle"; import EtaCard from "../components/EtaCard"; import QuickAction from "../components/QuickAction"; +import { useApp } from "../context/AppContext"; + export default function HomeScreen() { + const { user, eta, loading, advanceTruck } = useApp(); + const router = useRouter(); + + useEffect(() => { + if (user && !eta) { + advanceTruck(); + } + }, [user]); + + if (!user) { + return ; + } + + const minutes = eta ? Math.max(0, eta.etaMinutes) : 0; + const windowText = eta?.arrivalWindow + ? `Ventana: ${eta.arrivalWindow.from} - ${eta.arrivalWindow.to}` + : "Tira para actualizar"; + return ( + } > @@ -32,10 +57,10 @@ export default function HomeScreen() { marginLeft: 12, }} > - Monitoreo inteligente de recolección + {eta?.message ?? "Monitoreo inteligente de recolección"} - + @@ -46,9 +71,17 @@ export default function HomeScreen() { marginTop: 10, }} > - + router.push("/alerts")} + /> - + diff --git a/frontend/src/app/login.tsx b/frontend/src/app/login.tsx new file mode 100644 index 0000000..bfd7b93 --- /dev/null +++ b/frontend/src/app/login.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import { View, Text, StyleSheet, Alert } from "react-native"; +import { useRouter } from "expo-router"; +import InputField from "../components/InputField"; +import PrimaryButton from "../components/PrimaryButton"; +import { useApp } from "../context/AppContext"; + +export default function LoginScreen() { + const [email, setEmail] = useState("ana@test.com"); + const [password, setPassword] = useState("123456"); + const [submitting, setSubmitting] = useState(false); + const { login } = useApp(); + const router = useRouter(); + + const handleLogin = async () => { + try { + setSubmitting(true); + await login(email, password); + router.replace("/"); + } catch (err) { + Alert.alert("Error", err instanceof Error ? err.message : "Login failed"); + } finally { + setSubmitting(false); + } + }; + + return ( + + Iniciar sesión + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, padding: 24, justifyContent: "center" }, + title: { fontSize: 24, fontWeight: "bold", marginBottom: 20, textAlign: "center" }, +}); diff --git a/frontend/src/app/profile.tsx b/frontend/src/app/profile.tsx index 754f25d..2c1940f 100644 --- a/frontend/src/app/profile.tsx +++ b/frontend/src/app/profile.tsx @@ -1,15 +1,67 @@ -import { View, Text } from "react-native"; +import { View, Text, StyleSheet } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useRouter } from "expo-router"; + +import { COLORS } from "../constants/colors"; +import PrimaryButton from "../components/PrimaryButton"; + +import { useApp } from "../context/AppContext"; export default function ProfileScreen() { + const { user, logout } = useApp(); + const router = useRouter(); + + if (!user) { + return ( + + + No has iniciado sesión + router.push("/login")} + /> + + + ); + } + + const handleLogout = () => { + logout(); + router.replace("/login"); + }; + return ( - - Perfil - + + + Hola, {user.name} + {user.email} + + + + ); } + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + content: { + flex: 1, + padding: 24, + justifyContent: "center", + }, + title: { + fontSize: 22, + fontWeight: "bold", + textAlign: "center", + marginBottom: 6, + }, + email: { + fontSize: 14, + color: "#6B7280", + textAlign: "center", + marginBottom: 16, + }, +}); diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts new file mode 100644 index 0000000..f334310 --- /dev/null +++ b/frontend/src/config/api.ts @@ -0,0 +1 @@ +export const API_URL = "http://10.0.2.2:8080"; \ No newline at end of file diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx new file mode 100644 index 0000000..aa8bcac --- /dev/null +++ b/frontend/src/context/AppContext.tsx @@ -0,0 +1,102 @@ +import React, { + createContext, + useCallback, + useContext, + useState, + type ReactNode, +} from "react"; + +import { setAuthToken } from "../lib/api"; +import { login as loginService, type AuthUser } from "../services/auth.service"; +import { + sendGpsUpdate, + type BackendNotification, + type EtaResult, +} from "../services/tracking.service"; + +const SIMULATION_STEPS = [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:00:00Z" }, + { positionId: 2, lat: 20.5185, lng: -100.8450, speed: 45, timestamp: "2026-05-22T06:12:00Z" }, + { positionId: 3, lat: 20.5215, lng: -100.8142, speed: 22, timestamp: "2026-05-22T06:25:00Z" }, + { positionId: 4, lat: 20.5212, lng: -100.8175, speed: 15, timestamp: "2026-05-22T06:38:00Z" }, + { positionId: 5, lat: 20.5210, lng: -100.8210, speed: 0, timestamp: "2026-05-22T06:50:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:40:00Z" }, +]; + +interface AppContextValue { + user: AuthUser | null; + eta: EtaResult | null; + notifications: BackendNotification[]; + loading: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; + advanceTruck: () => Promise; +} + +const AppContext = createContext(null); + +export const useApp = (): AppContextValue => { + const ctx = useContext(AppContext); + if (!ctx) throw new Error("useApp must be used within AppProvider"); + return ctx; +}; + +export const AppProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [eta, setEta] = useState(null); + const [notifications, setNotifications] = useState([]); + const [stepIndex, setStepIndex] = useState(0); + const [loading, setLoading] = useState(false); + + const login = useCallback(async (email: string, password: string) => { + setLoading(true); + try { + const res = await loginService({ email, password }); + setUser(res.user); + } finally { + setLoading(false); + } + }, []); + + const logout = useCallback(() => { + setAuthToken(null); + setUser(null); + setEta(null); + setNotifications([]); + setStepIndex(0); + }, []); + + const advanceTruck = useCallback(async () => { + const step = SIMULATION_STEPS[stepIndex % SIMULATION_STEPS.length]; + setLoading(true); + try { + const res = await sendGpsUpdate({ + truckId: "101", + routeId: "RUTA-01", + status: "EN_RUTA", + ...step, + }); + setEta(res.eta); + + // Sólo mostramos las notificaciones que pertenecen al usuario logueado. + const myNotifications = user + ? res.notifications.filter((n) => n.userId === user.id) + : []; + + if (myNotifications.length > 0) { + setNotifications((prev) => [...myNotifications, ...prev]); + } + setStepIndex((i) => i + 1); + } finally { + setLoading(false); + } + }, [stepIndex, user]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..428c54f --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,30 @@ +import { API_URL } from "../config/api"; + +let authToken: string | null = null; + +export const setAuthToken = (token: string | null) => { + authToken = token; +}; + +export const apiFetch = async ( + path: string, + options: RequestInit = {}, +): Promise => { + const headers: Record = { + "Content-Type": "application/json", + ...(options.headers as Record), + }; + + if (authToken) { + headers.Authorization = `Bearer ${authToken}`; + } + + const res = await fetch(`${API_URL}${path}`, { ...options, headers }); + const body = await res.json().catch(() => ({})); + + if (!res.ok) { + throw new Error(body.error ?? `HTTP ${res.status}`); + } + + return body as T; +}; \ No newline at end of file diff --git a/frontend/src/lib/notification-mapper.ts b/frontend/src/lib/notification-mapper.ts new file mode 100644 index 0000000..eec20c0 --- /dev/null +++ b/frontend/src/lib/notification-mapper.ts @@ -0,0 +1,14 @@ +import type { NotificationType } from "../services/tracking.service"; + +export const notificationTypeToAlertType = ( + type: NotificationType, +): "started" | "near" | "danger" | "completed" => { + switch (type) { + case "ROUTE_START": return "started"; + case "TRUCK_PROXIMITY": return "near"; + case "TRUCK_ARRIVED": return "near"; + case "ROUTE_COMPLETED": return "completed"; + case "DELAY": return "danger"; + case "MECHANICAL_FAILURE": return "danger"; + } +}; \ No newline at end of file diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts new file mode 100644 index 0000000..867caa6 --- /dev/null +++ b/frontend/src/services/auth.service.ts @@ -0,0 +1,25 @@ +import { apiFetch, setAuthToken } from "../lib/api"; + +export interface AuthUser { + id: number; + email: string; + name: string; +} + +export const register = (data: { name: string; email: string; password: string }) => + apiFetch<{ id: number; email: string; name: string }>("/api/auth/register", { + method: "POST", + body: JSON.stringify(data), + }); + +export const login = async (data: { email: string; password: string }) => { + const res = await apiFetch<{ user: AuthUser; token: string }>("/api/auth/login", { + method: "POST", + body: JSON.stringify(data), + }); + setAuthToken(res.token); + return res; +}; + +export const getMe = () => + apiFetch("/api/auth/me"); \ No newline at end of file diff --git a/frontend/src/services/tracking.service.ts b/frontend/src/services/tracking.service.ts new file mode 100644 index 0000000..b312f11 --- /dev/null +++ b/frontend/src/services/tracking.service.ts @@ -0,0 +1,46 @@ +import { apiFetch } from "../lib/api"; + +export type NotificationType = + | "ROUTE_START" + | "TRUCK_PROXIMITY" + | "TRUCK_ARRIVED" + | "ROUTE_COMPLETED" + | "DELAY" + | "MECHANICAL_FAILURE"; + +export interface GpsUpdatePayload { + truckId: string; + routeId: string; + lat: number; + lng: number; + speed: number; + status: "EN_RUTA" | "DETENIDO" | "FINALIZADO" | "FALLA"; + positionId?: number; + timestamp?: string; +} + +export interface EtaResult { + etaMinutes: number; + arrivalWindow: { from: string; to: string }; + message: string; +} + +export interface BackendNotification { + userId: number; + type: NotificationType; + title: string; + body: string; +} + +export interface GpsUpdateResponse { + message: string; + truck: { truckId: number; routeId: string; status: string }; + eta: EtaResult; + notifications: BackendNotification[]; +} + +export const sendGpsUpdate = (payload: GpsUpdatePayload) => + apiFetch("/api/tracking/gps-update", { + method: "POST", + body: JSON.stringify(payload), + }); \ No newline at end of file