diff --git a/frontend/assets/illustrations/avatar-ana.png b/frontend/assets/illustrations/avatar-ana.png new file mode 100644 index 0000000..4e066d7 Binary files /dev/null and b/frontend/assets/illustrations/avatar-ana.png differ diff --git a/frontend/assets/illustrations/battery-meds.png b/frontend/assets/illustrations/battery-meds.png new file mode 100644 index 0000000..963e525 Binary files /dev/null and b/frontend/assets/illustrations/battery-meds.png differ diff --git a/frontend/assets/illustrations/bins-cute.png b/frontend/assets/illustrations/bins-cute.png new file mode 100644 index 0000000..ffcadd6 Binary files /dev/null and b/frontend/assets/illustrations/bins-cute.png differ diff --git a/frontend/assets/illustrations/eco-bins.png b/frontend/assets/illustrations/eco-bins.png new file mode 100644 index 0000000..ac0e7a9 Binary files /dev/null and b/frontend/assets/illustrations/eco-bins.png differ diff --git a/frontend/assets/illustrations/garbage-truck.png b/frontend/assets/illustrations/garbage-truck.png new file mode 100644 index 0000000..6af7ad5 Binary files /dev/null and b/frontend/assets/illustrations/garbage-truck.png differ diff --git a/frontend/assets/illustrations/impact-leaf.png b/frontend/assets/illustrations/impact-leaf.png new file mode 100644 index 0000000..d80a4c0 Binary files /dev/null and b/frontend/assets/illustrations/impact-leaf.png differ diff --git a/frontend/assets/illustrations/organic-food.png b/frontend/assets/illustrations/organic-food.png new file mode 100644 index 0000000..f5fb69f Binary files /dev/null and b/frontend/assets/illustrations/organic-food.png differ diff --git a/frontend/assets/illustrations/pet-bottles.png b/frontend/assets/illustrations/pet-bottles.png new file mode 100644 index 0000000..7657d16 Binary files /dev/null and b/frontend/assets/illustrations/pet-bottles.png differ diff --git a/frontend/assets/illustrations/recycling.png b/frontend/assets/illustrations/recycling.png new file mode 100644 index 0000000..5333384 Binary files /dev/null and b/frontend/assets/illustrations/recycling.png differ diff --git a/frontend/assets/illustrations/sanitary.png b/frontend/assets/illustrations/sanitary.png new file mode 100644 index 0000000..7fc7008 Binary files /dev/null and b/frontend/assets/illustrations/sanitary.png differ diff --git a/frontend/assets/illustrations/trees-banner.png b/frontend/assets/illustrations/trees-banner.png new file mode 100644 index 0000000..7db57fa Binary files /dev/null and b/frontend/assets/illustrations/trees-banner.png differ diff --git a/frontend/assets/illustrations/truck-banner.png b/frontend/assets/illustrations/truck-banner.png new file mode 100644 index 0000000..a074e4d Binary files /dev/null and b/frontend/assets/illustrations/truck-banner.png differ diff --git a/frontend/assets/illustrations/truck-city.png b/frontend/assets/illustrations/truck-city.png new file mode 100644 index 0000000..becdd3e Binary files /dev/null and b/frontend/assets/illustrations/truck-city.png differ diff --git a/frontend/src/app/alerts.tsx b/frontend/src/app/alerts.tsx index 5228c8f..47b1393 100644 --- a/frontend/src/app/alerts.tsx +++ b/frontend/src/app/alerts.tsx @@ -1,64 +1,392 @@ -import { ScrollView, Text } from "react-native"; +import { ScrollView, View, Text, StyleSheet, Image } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { Ionicons } from "@expo/vector-icons"; 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"; +import type { NotificationType } from "../services/tracking.service"; + +type Severity = "info" | "attention" | "important" | "critical"; + +const meta: Record< + NotificationType, + { + severity: Severity; + icon: keyof typeof Ionicons.glyphMap; + color: string; + bg: string; + badge: string; + } +> = { + ROUTE_START: { + severity: "info", + icon: "navigate", + color: "#3B82F6", + bg: "#EFF6FF", + badge: "Informativo", + }, + TRUCK_PROXIMITY: { + severity: "attention", + icon: "time-outline", + color: "#F59E0B", + bg: "#FEF3C7", + badge: "Atención", + }, + TRUCK_ARRIVED: { + severity: "important", + icon: "notifications", + color: "#22C55E", + bg: "#ECFDF5", + badge: "Importante", + }, + ROUTE_COMPLETED: { + severity: "info", + icon: "checkmark-done", + color: "#6B7280", + bg: "#F3F4F6", + badge: "Informativo", + }, + DELAY: { + severity: "critical", + icon: "warning-outline", + color: "#EF4444", + bg: "#FEF2F2", + badge: "Crítico", + }, + MECHANICAL_FAILURE: { + severity: "critical", + icon: "warning", + color: "#EF4444", + bg: "#FEF2F2", + badge: "Crítico", + }, +}; + +const STATUS_LABEL: Record = { + EN_RUTA: { label: "ACTIVA", color: "#22C55E" }, + DETENIDO: { label: "DETENIDO", color: "#F59E0B" }, + FALLA: { label: "FALLA", color: "#EF4444" }, + FINALIZADO: { label: "FINALIZADO", color: "#6B7280" }, + ESPERA: { label: "EN ESPERA", color: "#6B7280" }, +}; export default function AlertsScreen() { - const { user, notifications } = useApp(); + const { user, notifications, route } = useApp(); - if (!user) { - return ; - } + if (!user) return ; - return ( - + + {/* Hero con título + count + camion image en banner azul */} + + + Alertas + + {notifications.length}{" "} + {notifications.length === 1 + ? "notificación" + : "notificaciones"} + + + + + + + + {/* Estado de ruta */} + - - + + + + + + ESTADO DE RUTA: {statusInfo.label} + + + {statusInfo.label === "ACTIVA" + ? "El servicio de recolección sigue en curso." + : "Sin servicio activo en este momento."} + + + - - {notifications.length > 0 - ? `${notifications.length} notificaciones` - : "Aún no hay alertas. Vuelve al inicio y desliza para actualizar."} - + {/* Timeline de notificaciones */} + {notifications.length === 0 ? ( + + + + Aún no hay alertas. Las recibirás cuando el camión avance en + tu ruta. + + + ) : ( + + {notifications.map((n, idx) => { + const m = meta[n.type]; + const isLast = idx === notifications.length - 1; + const timeStr = new Date(n.createdAt).toLocaleTimeString( + "es-MX", + { hour: "2-digit", minute: "2-digit" }, + ); + return ( + + {/* Columna izquierda: dot + línea */} + + + + + {!isLast && ( + + )} + {timeStr} + - {notifications.map((n) => ( - - ))} - - - ); + {/* Card */} + + + {n.title} + + + {m.badge} + + + + {n.body} + + + ); + })} + + )} + + {/* Consejo del día */} + + + + + + Consejo del día + + Separa correctamente tus residuos. Tu acción hace la diferencia. + + + + + + + ); } + +const styles = StyleSheet.create({ + heroSection: { + backgroundColor: "#DBEAFE", + paddingBottom: 0, + marginBottom: 20, + }, + heroTextWrap: { + paddingHorizontal: 20, + paddingTop: 12, + paddingBottom: 8, + }, + heroBanner: { + height: 150, + width: "100%", + backgroundColor: "#DBEAFE", + overflow: "hidden", + }, + headerTitle: { + fontSize: 32, + fontWeight: "800", + color: "#0F172A", + }, + headerCount: { + fontSize: 14, + color: "#6B7280", + marginTop: 2, + }, + truckImage: { + width: "100%", + height: "100%", + }, + statusBanner: { + flexDirection: "row", + alignItems: "center", + marginHorizontal: 20, + padding: 14, + borderRadius: 14, + marginBottom: 4, + }, + statusIcon: { + width: 38, + height: 38, + borderRadius: 19, + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + statusTitle: { + fontSize: 13, + fontWeight: "800", + }, + statusBody: { + fontSize: 12, + color: "#374151", + marginTop: 2, + }, + emptyBox: { + alignItems: "center", + padding: 40, + marginHorizontal: 20, + marginTop: 16, + backgroundColor: "#FFFFFF", + borderRadius: 14, + }, + emptyText: { + fontSize: 13, + color: "#6B7280", + textAlign: "center", + marginTop: 10, + }, + timelineRow: { + flexDirection: "row", + marginBottom: 14, + }, + timelineLeft: { + width: 56, + alignItems: "center", + }, + timelineDot: { + width: 32, + height: 32, + borderRadius: 16, + justifyContent: "center", + alignItems: "center", + }, + timelineLine: { + flex: 1, + width: 2, + marginTop: 2, + marginBottom: 2, + }, + timelineTime: { + fontSize: 10, + color: "#9CA3AF", + marginTop: 4, + }, + notifCard: { + flex: 1, + borderRadius: 14, + padding: 14, + marginLeft: 4, + }, + notifHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + marginBottom: 6, + }, + notifTitle: { + fontSize: 14, + fontWeight: "800", + color: "#0F172A", + flex: 1, + paddingRight: 8, + }, + badge: { + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 8, + }, + badgeText: { + fontSize: 10, + fontWeight: "700", + }, + notifBody: { + fontSize: 13, + color: "#374151", + lineHeight: 18, + }, + tipCard: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#ECFDF5", + borderRadius: 16, + padding: 14, + marginHorizontal: 20, + marginTop: 16, + }, + tipIcon: { + width: 42, + height: 42, + borderRadius: 21, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + tipTitle: { + fontSize: 14, + fontWeight: "800", + color: "#065F46", + marginBottom: 4, + }, + tipBody: { + fontSize: 12, + color: "#065F46", + lineHeight: 16, + }, + tipImage: { + width: 70, + height: 70, + marginLeft: 8, + }, +}); diff --git a/frontend/src/app/guide.tsx b/frontend/src/app/guide.tsx index 265fc2b..7175431 100644 --- a/frontend/src/app/guide.tsx +++ b/frontend/src/app/guide.tsx @@ -1,15 +1,16 @@ -import { ScrollView, View, Text, StyleSheet } from "react-native"; +import { ScrollView, View, Text, StyleSheet, Image } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { Ionicons } from "@expo/vector-icons"; import { COLORS } from "../constants/colors"; -import SectionTitle from "../components/SectionTitle"; type Category = { key: string; title: string; color: string; + bg: string; icon: keyof typeof Ionicons.glyphMap; + image: any; examples: string[]; tip: string; }; @@ -19,7 +20,9 @@ const CATEGORIES: Category[] = [ key: "organico", title: "Orgánicos", color: "#22C55E", + bg: "#ECFDF5", icon: "leaf", + image: require("../../assets/illustrations/organic-food.png"), examples: [ "Restos de comida (frutas, verduras, cáscaras)", "Bolsitas de té, residuos de café", @@ -31,7 +34,9 @@ const CATEGORIES: Category[] = [ key: "reciclable", title: "Reciclables", color: "#3B82F6", + bg: "#EFF6FF", icon: "refresh-circle", + image: require("../../assets/illustrations/pet-bottles.png"), examples: [ "Botellas y envases PET limpios", "Cartón y papel seco", @@ -42,8 +47,10 @@ const CATEGORIES: Category[] = [ { key: "sanitario", title: "Sanitarios", - color: "#9CA3AF", + color: "#6B7280", + bg: "#F3F4F6", icon: "medkit", + image: require("../../assets/illustrations/sanitary.png"), examples: [ "Pañales y toallas femeninas", "Papel higiénico usado", @@ -55,7 +62,9 @@ const CATEGORIES: Category[] = [ key: "especial", title: "Especiales", color: "#EF4444", + bg: "#FEF2F2", icon: "warning", + image: require("../../assets/illustrations/battery-meds.png"), examples: [ "Pilas y baterías", "Electrónicos viejos", @@ -68,39 +77,51 @@ const CATEGORIES: Category[] = [ export default function GuideScreen() { return ( - + - - - - Separar correctamente reduce contaminación y ayuda a la ruta de - recolección. Funciona sin conexión. - + {/* Header con título grande + imagen */} + + + Guía de{"\n"}separación + + Separar correctamente reduce contaminación y ayuda a la ruta de + recolección. + + Funciona sin conexión. + + + {CATEGORIES.map((cat) => ( + {/* Icono circular */} + + {/* Contenido */} - - {cat.title} - + + + {cat.title} + + + {cat.examples.map((ex, i) => ( • {ex} @@ -109,24 +130,50 @@ export default function GuideScreen() { + - 💡 {cat.tip} + {cat.tip} + + {/* Imagen ilustrativa por categoría */} + ))} + {/* Recuerda */} - Recuerda - - • Saca tu basura sólo dentro del horario de recolección.{"\n"} - • No persigas al camión, es peligroso.{"\n"} - • Si tu camión no pasó, usa el buzón de retroalimentación. - + + + + + + Recuerda + + {[ + "Saca tu basura sólo dentro del horario.", + "No persigas al camión, es peligroso.", + "Si tu camión no pasó, usa el buzón.", + ].map((t, i) => ( + + + {t} + + ))} + + @@ -134,11 +181,40 @@ export default function GuideScreen() { } const styles = StyleSheet.create({ + headerRow: { + flexDirection: "row", + alignItems: "center", + marginBottom: 20, + paddingHorizontal: 4, + }, + headerTitle: { + fontSize: 30, + fontWeight: "800", + color: "#0F172A", + lineHeight: 34, + marginBottom: 8, + }, + headerSubtitle: { + fontSize: 13, + color: "#1E40AF", + lineHeight: 18, + marginBottom: 4, + }, + headerOffline: { + fontSize: 13, + color: "#0E8A61", + fontWeight: "700", + }, + heroImage: { + width: 120, + height: 120, + }, card: { flexDirection: "row", + alignItems: "flex-start", backgroundColor: "#FFFFFF", borderRadius: 18, - padding: 16, + padding: 14, marginBottom: 14, shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, @@ -152,28 +228,82 @@ const styles = StyleSheet.create({ borderRadius: 25, justifyContent: "center", alignItems: "center", - marginRight: 14, + marginRight: 12, }, - cardContent: { flex: 1 }, - cardTitle: { fontSize: 17, fontWeight: "800", marginBottom: 6 }, - example: { fontSize: 13, color: "#4B5563", marginBottom: 3 }, + cardContent: { + flex: 1, + paddingRight: 4, + }, + cardTitleRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 6, + }, + cardTitle: { fontSize: 18, fontWeight: "800" }, + example: { fontSize: 12, color: "#4B5563", marginBottom: 2 }, tipBox: { - marginTop: 10, - padding: 10, - borderRadius: 10, - }, - tipText: { fontSize: 12, fontWeight: "600" }, - preventiveBox: { + flexDirection: "row", + alignItems: "center", marginTop: 8, - padding: 16, + padding: 8, + borderRadius: 8, + }, + tipText: { + fontSize: 11, + fontWeight: "600", + marginLeft: 6, + flex: 1, + lineHeight: 14, + }, + categoryImage: { + width: 64, + height: 80, + marginLeft: 4, + alignSelf: "center", + }, + preventiveBox: { + flexDirection: "row", + alignItems: "center", + marginTop: 8, + padding: 14, backgroundColor: "#FEF3C7", borderRadius: 14, }, + preventiveHeader: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, + preventiveIcon: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "#F59E0B", + justifyContent: "center", + alignItems: "center", + marginRight: 10, + }, preventiveTitle: { - fontSize: 14, + fontSize: 16, fontWeight: "800", color: "#92400E", - marginBottom: 6, }, - preventiveBody: { fontSize: 13, color: "#92400E", lineHeight: 20 }, + preventiveRow: { + flexDirection: "row", + alignItems: "center", + marginBottom: 4, + }, + preventiveBody: { + fontSize: 12, + color: "#92400E", + marginLeft: 6, + flex: 1, + lineHeight: 16, + }, + preventiveImage: { + width: 90, + height: 90, + marginLeft: 4, + }, }); diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 0bc880f..dc6cf6d 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -1,127 +1,473 @@ -import { ScrollView, View, Text, RefreshControl } from "react-native"; +import { useEffect } from "react"; +import { + ScrollView, + View, + Text, + Image, + Pressable, + StyleSheet, +} from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { Ionicons } from "@expo/vector-icons"; import { Redirect, useRouter } from "expo-router"; import { COLORS } from "../constants/colors"; - -import SectionTitle from "../components/SectionTitle"; -import EtaCard from "../components/EtaCard"; -import QuickAction from "../components/QuickAction"; - import { useApp } from "../context/AppContext"; +type QuickAction = { + key: string; + title: string; + icon: keyof typeof Ionicons.glyphMap; + color: string; + bg: string; + onPress: () => void; +}; + export default function HomeScreen() { const { user, eta, route, loading, refreshStatus } = useApp(); const router = useRouter(); - if (!user) { - return ; - } + useEffect(() => { + if (user && !eta) { + void refreshStatus(); + } + }, [user]); - const minutes = eta ? Math.max(0, eta.etaMinutes) : 0; + if (!user) return ; + + const minutes = eta ? Math.max(0, eta.etaMinutes) : null; const windowText = eta?.arrivalWindow - ? `Ventana: ${eta.arrivalWindow.from} - ${eta.arrivalWindow.to}` - : "Esperando datos del camión..."; + ? `${eta.arrivalWindow.from} - ${eta.arrivalWindow.to} a.m.` + : "Calculando..."; - const subtitle = - eta?.message ?? `Monitoreando ruta ${route?.routeId ?? "..."}`; + const statusOK = route?.status === "EN_RUTA" || route?.status === "ESPERA"; + const statusColor = statusOK ? "#22C55E" : "#EF4444"; + const statusLabel = + route?.status === "EN_RUTA" + ? "ESTABLE" + : route?.status === "FALLA" + ? "CON FALLA" + : route?.status === "DETENIDO" + ? "DETENIDO" + : "EN ESPERA"; + + const quickActions: QuickAction[] = [ + { + key: "alerts", + title: "Alertas", + icon: "notifications-outline", + color: "#22C55E", + bg: "#ECFDF5", + onPress: () => router.push("/alerts"), + }, + { + key: "guide", + title: "Guía", + icon: "leaf-outline", + color: "#3B82F6", + bg: "#EFF6FF", + onPress: () => router.push("/guide"), + }, + { + key: "feedback", + title: "Reportar", + icon: "chatbubble-outline", + color: "#EF4444", + bg: "#FEF2F2", + onPress: () => router.push("/feedback"), + }, + { + key: "address", + title: "Domicilio", + icon: "home-outline", + color: "#8B5CF6", + bg: "#F5F3FF", + onPress: () => router.push("/addresses"), + }, + ]; return ( - } + contentContainerStyle={{ paddingBottom: 130 }} + refreshControl={undefined} > - - - - {subtitle} - - - - - {route?.horarioEstimado && ( - - - PRÓXIMA RECOLECCIÓN - - - {route.horarioEstimado} + {/* Title */} + + + EcoRuta + + Ruta {route?.routeId ?? "—"} - )} - - - - ⚠️ No saques tus residuos fuera del horario y no persigas al camión. - - - - - router.push("/alerts")} + {/* Hero del camión */} + + + - router.push("/feedback")} + {/* Card ETA */} + + + + + + + LLEGADA ESTIMADA + + {minutes !== null + ? `${minutes} min` + : loading + ? "..." + : "—"} + + Ventana de recolección + + {windowText} + + + + + + + + + + + {/* Estado de ruta */} + + + + + ESTADO DE RUTA: {statusLabel} + + + {statusOK + ? "Tu recolección sigue en horario normal." + : "Hay incidencias en la ruta."} + + + + + + + + {/* Mensaje preventivo */} + + + + + + No saques tus residuos fuera del horario{"\n"}y no persigas al camión. + + + + + + + {/* Acciones rápidas */} + ACCIONES RÁPIDAS + + {quickActions.map((a) => ( + + + + + {a.title} + + ))} + + + {/* Consejo rápido */} + + + + + + Consejo rápido + + Compacta cartón y PET para ahorrar espacio y ayudar al medio + ambiente. + + + ); } + +const styles = StyleSheet.create({ + titleRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 20, + paddingTop: 8, + }, + title: { + fontSize: 32, + fontWeight: "800", + color: "#0E8A61", + }, + subtitle: { + fontSize: 14, + color: "#6B7280", + marginTop: 2, + }, + heroBox: { + height: 180, + marginTop: 8, + backgroundColor: "#DBEAFE", + overflow: "hidden", + }, + heroTruck: { + width: "100%", + height: "100%", + }, + etaCard: { + marginHorizontal: 20, + marginTop: -30, + backgroundColor: "#ECFDF5", + borderRadius: 20, + padding: 18, + shadowColor: "#000", + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.08, + shadowRadius: 10, + elevation: 6, + }, + etaContent: { + flexDirection: "row", + alignItems: "center", + }, + etaIcon: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + etaLabel: { + fontSize: 11, + fontWeight: "800", + color: "#065F46", + letterSpacing: 0.5, + }, + etaMinutes: { + fontSize: 36, + fontWeight: "800", + color: "#0F172A", + marginTop: 4, + lineHeight: 38, + }, + etaWindowLabel: { + fontSize: 11, + color: "#6B7280", + marginTop: 6, + }, + windowPill: { + alignSelf: "flex-start", + backgroundColor: "#D1FAE5", + borderRadius: 10, + paddingHorizontal: 10, + paddingVertical: 4, + marginTop: 4, + }, + windowText: { + fontSize: 13, + color: "#065F46", + fontWeight: "700", + }, + etaCircle: { + width: 88, + height: 88, + borderRadius: 44, + borderWidth: 4, + borderColor: "#22C55E", + justifyContent: "center", + alignItems: "center", + marginLeft: 8, + }, + etaCircleInner: { + width: 64, + height: 64, + borderRadius: 32, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + }, + statusBanner: { + flexDirection: "row", + alignItems: "center", + marginHorizontal: 20, + marginTop: 14, + backgroundColor: "#FFFFFF", + borderRadius: 14, + padding: 14, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + statusDot: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: 12, + }, + statusTitle: { + fontSize: 13, + fontWeight: "800", + }, + statusBody: { + fontSize: 12, + color: "#374151", + marginTop: 2, + }, + statusShield: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: "center", + alignItems: "center", + marginLeft: 8, + }, + warningBanner: { + flexDirection: "row", + alignItems: "center", + marginHorizontal: 20, + marginTop: 12, + backgroundColor: "#FEF3C7", + borderRadius: 14, + padding: 12, + }, + warningIcon: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "#F59E0B", + justifyContent: "center", + alignItems: "center", + marginRight: 10, + }, + warningText: { + flex: 1, + fontSize: 12, + color: "#92400E", + fontWeight: "600", + lineHeight: 16, + }, + warningRight: { + marginLeft: 8, + }, + sectionTitle: { + fontSize: 12, + fontWeight: "800", + color: "#6B7280", + letterSpacing: 1, + marginTop: 22, + marginBottom: 10, + marginLeft: 20, + }, + actionsGrid: { + flexDirection: "row", + justifyContent: "space-between", + paddingHorizontal: 20, + }, + actionCard: { + flex: 1, + backgroundColor: "#FFFFFF", + borderRadius: 14, + paddingVertical: 14, + alignItems: "center", + marginHorizontal: 4, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + actionIcon: { + width: 50, + height: 50, + borderRadius: 25, + justifyContent: "center", + alignItems: "center", + marginBottom: 8, + }, + actionTitle: { + fontSize: 12, + fontWeight: "700", + color: "#0F172A", + textAlign: "center", + }, + tipCard: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#ECFDF5", + borderRadius: 16, + padding: 14, + marginHorizontal: 20, + marginTop: 16, + }, + tipIcon: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + tipTitle: { + fontSize: 14, + fontWeight: "800", + color: "#065F46", + marginBottom: 2, + }, + tipBody: { + fontSize: 12, + color: "#065F46", + lineHeight: 16, + }, + tipImage: { + width: 60, + height: 60, + marginLeft: 8, + }, +}); diff --git a/frontend/src/app/profile.tsx b/frontend/src/app/profile.tsx index 0d9191f..e6c40fa 100644 --- a/frontend/src/app/profile.tsx +++ b/frontend/src/app/profile.tsx @@ -1,104 +1,382 @@ -import { View, Text, StyleSheet, Alert } from "react-native"; +import { useEffect, useState } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + Image, + Pressable, + Alert, +} from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { useRouter } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import { Redirect, useRouter } from "expo-router"; import { COLORS } from "../constants/colors"; -import PrimaryButton from "../components/PrimaryButton"; - import { useApp } from "../context/AppContext"; -import { resetDemo } from "../services/tracking.service"; +import { getMyAddress, type MyAddress } from "../services/addresses.service"; +import { apiFetch } from "../lib/api"; + +type Row = { + key: string; + title: string; + subtitle: string; + icon: keyof typeof Ionicons.glyphMap; + color: string; + onPress: () => void; + danger?: boolean; +}; export default function ProfileScreen() { - const { user, logout, refreshStatus } = useApp(); - const router = useRouter(); + const { user, logout } = useApp(); + const router = useRouter(); + const [myAddress, setMyAddress] = useState(null); - const handleResetDemo = async () => { - try { - await resetDemo(); - await refreshStatus(); - Alert.alert("Demo reiniciado", "Estado y notificaciones borradas."); - } catch (err) { - Alert.alert( - "Error", - err instanceof Error ? err.message : "No se pudo reiniciar", - ); - } - }; + useEffect(() => { + if (!user) return; + void getMyAddress().then(setMyAddress).catch(() => {}); + }, [user]); - if (!user) { - return ( - - - No has iniciado sesión - router.push("/login")} - /> - - - ); + if (!user) return ; + + const handleLogout = () => { + logout(); + router.replace("/login"); + }; + + const handleReset = async () => { + try { + await apiFetch<{ message: string }>("/api/tracking/reset-demo", { + method: "POST", + }); + Alert.alert("Demo reiniciada", "Las notificaciones se borraron."); + } catch (err) { + Alert.alert( + "No se pudo reiniciar", + err instanceof Error ? err.message : "Error", + ); } + }; - const handleLogout = () => { - logout(); - router.replace("/login"); - }; - - return ( - - - Hola, {user.name} - {user.email} - - - - router.push("/addresses")} - /> - - - - router.push("/feedback")} - /> - - - - - - - - - - + const handleHelp = () => { + Alert.alert( + "Ayuda", + "EcoRuta te avisa cuando el camión recolector está cerca.\n\n" + + "• Tu dirección define la ruta que verás.\n" + + "• Las alertas se actualizan cada 30 segundos.\n" + + "• Usa el buzón si el camión no pasó.", ); + }; + + const rows: Row[] = [ + { + key: "address", + title: "Mi domicilio", + subtitle: "Ver y administrar tus domicilios registrados", + icon: "home-outline", + color: "#0E8A61", + onPress: () => router.push("/addresses"), + }, + { + key: "feedback", + title: "Buzón de retroalimentación", + subtitle: "Sugerencias, comentarios y reportes", + icon: "chatbubble-outline", + color: "#0E8A61", + onPress: () => router.push("/feedback"), + }, + { + key: "reset", + title: "Reiniciar demo", + subtitle: "Restablecer la simulación y los datos", + icon: "refresh-outline", + color: "#0E8A61", + onPress: handleReset, + }, + { + key: "help", + title: "Ayuda", + subtitle: "Preguntas frecuentes y soporte", + icon: "help-circle-outline", + color: "#0E8A61", + onPress: handleHelp, + }, + { + key: "logout", + title: "Cerrar sesión", + subtitle: "Salir de tu cuenta", + icon: "log-out-outline", + color: "#EF4444", + onPress: handleLogout, + danger: true, + }, + ]; + + const initial = (user.name?.[0] ?? user.email[0]).toUpperCase(); + + return ( + + + {/* Header verde con avatar */} + + + + + {initial} + + + + + + + Hola, {user.name} 🌿 + + + + {user.email} + + + + {/* Zona + ruta */} + router.push("/addresses")} + > + + + + Tu zona + + {myAddress?.colonia ?? "Sin asignar"} + + + + + + + + Ruta asignada + + {myAddress?.routeId ?? "—"} + + + + + + + {/* Lista de opciones */} + + {rows.map((r) => ( + + + + + + + {r.title} + + {r.subtitle} + + + + ))} + + + {/* Tu impacto cuenta */} + + + + + + Tu impacto cuenta + + Siguiendo los horarios y separando correctamente, haces tu ciudad + más limpia. + + + + + + ); } 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, - }, + heroWrap: { + height: 140, + backgroundColor: "#D1FAE5", + overflow: "visible", + marginBottom: 60, + }, + heroBackground: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "#D1FAE5", + }, + avatarOuter: { + position: "absolute", + bottom: -50, + alignSelf: "center", + width: 112, + height: 112, + borderRadius: 56, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 6, + }, + avatarInner: { + width: 100, + height: 100, + borderRadius: 50, + backgroundColor: "#0E8A61", + justifyContent: "center", + alignItems: "center", + }, + avatarText: { + color: "#FFFFFF", + fontSize: 38, + fontWeight: "800", + }, + identityBox: { + alignItems: "center", + marginBottom: 18, + }, + name: { + fontSize: 22, + fontWeight: "800", + color: "#0F172A", + }, + emailRow: { + flexDirection: "row", + alignItems: "center", + marginTop: 6, + }, + email: { + fontSize: 13, + color: "#6B7280", + marginLeft: 6, + }, + zoneRow: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#ECFDF5", + borderRadius: 16, + padding: 14, + marginHorizontal: 16, + marginBottom: 16, + }, + zoneCol: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + zoneDivider: { + width: 1, + height: 36, + backgroundColor: "#A7F3D0", + marginHorizontal: 8, + }, + zoneLabel: { + fontSize: 11, + color: "#065F46", + fontWeight: "700", + }, + zoneValue: { + fontSize: 13, + color: "#0F172A", + fontWeight: "600", + marginTop: 2, + }, + rowCard: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#FFFFFF", + borderRadius: 14, + padding: 14, + marginBottom: 10, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 6, + elevation: 2, + }, + rowIcon: { + width: 42, + height: 42, + borderRadius: 21, + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + rowTitle: { + fontSize: 15, + fontWeight: "700", + color: "#0F172A", + }, + rowSubtitle: { + fontSize: 12, + color: "#6B7280", + marginTop: 2, + }, + impactCard: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#ECFDF5", + borderRadius: 16, + padding: 14, + marginHorizontal: 16, + marginTop: 8, + }, + impactLeaf: { + width: 50, + height: 50, + borderRadius: 25, + backgroundColor: "#FFFFFF", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + impactTitle: { + fontSize: 14, + fontWeight: "800", + color: "#065F46", + marginBottom: 4, + }, + impactBody: { + fontSize: 12, + color: "#065F46", + lineHeight: 16, + }, }); diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 567bd76..e391e21 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -1,3 +1,3 @@ // android -// export const API_URL = "http://10.0.2.2:8080"; -export const API_URL = "http://192.168.93.148:8080"; \ No newline at end of file + export const API_URL = "http://10.0.2.2:8080"; +//export const API_URL = "http://172.20.10.4:8080"; \ No newline at end of file