feat: add UI screens design
BIN
frontend/assets/illustrations/avatar-ana.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/assets/illustrations/battery-meds.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/assets/illustrations/bins-cute.png
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
frontend/assets/illustrations/eco-bins.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
frontend/assets/illustrations/garbage-truck.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/assets/illustrations/impact-leaf.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
frontend/assets/illustrations/organic-food.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
frontend/assets/illustrations/pet-bottles.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/assets/illustrations/recycling.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/assets/illustrations/sanitary.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
frontend/assets/illustrations/trees-banner.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/assets/illustrations/truck-banner.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
frontend/assets/illustrations/truck-city.png
Normal file
|
After Width: | Height: | Size: 964 KiB |
@@ -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<string, { label: string; color: string }> = {
|
||||
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 <Redirect href="/login" />;
|
||||
}
|
||||
if (!user) return <Redirect href="/login" />;
|
||||
|
||||
const statusInfo =
|
||||
STATUS_LABEL[route?.status ?? "ESPERA"] ?? STATUS_LABEL.ESPERA;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
}}
|
||||
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||
edges={["top"]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
paddingBottom: 120,
|
||||
}}
|
||||
contentContainerStyle={{ paddingBottom: 130 }}
|
||||
>
|
||||
<SectionTitle title="Alertas" />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#6B7280",
|
||||
fontSize: 14,
|
||||
marginBottom: 24,
|
||||
marginLeft: 12,
|
||||
}}
|
||||
>
|
||||
{notifications.length > 0
|
||||
? `${notifications.length} notificaciones`
|
||||
: "Aún no hay alertas. Vuelve al inicio y desliza para actualizar."}
|
||||
{/* Hero con título + count + camion image en banner azul */}
|
||||
<View style={styles.heroSection}>
|
||||
<View style={styles.heroTextWrap}>
|
||||
<Text style={styles.headerTitle}>Alertas</Text>
|
||||
<Text style={styles.headerCount}>
|
||||
{notifications.length}{" "}
|
||||
{notifications.length === 1
|
||||
? "notificación"
|
||||
: "notificaciones"}
|
||||
</Text>
|
||||
|
||||
{notifications.map((n) => (
|
||||
<AlertItem
|
||||
key={n.id}
|
||||
title={n.title}
|
||||
description={n.body}
|
||||
time={new Date(n.createdAt).toLocaleTimeString("es-MX", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
type={notificationTypeToAlertType(n.type)}
|
||||
</View>
|
||||
<View style={styles.heroBanner}>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/truck-city.png")}
|
||||
style={styles.truckImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Estado de ruta */}
|
||||
<View
|
||||
style={[
|
||||
styles.statusBanner,
|
||||
{ backgroundColor: `${statusInfo.color}15` },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.statusIcon,
|
||||
{ backgroundColor: statusInfo.color },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="bus-outline" size={20} color="#FFFFFF" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.statusTitle, { color: statusInfo.color }]}>
|
||||
ESTADO DE RUTA: {statusInfo.label}
|
||||
</Text>
|
||||
<Text style={styles.statusBody}>
|
||||
{statusInfo.label === "ACTIVA"
|
||||
? "El servicio de recolección sigue en curso."
|
||||
: "Sin servicio activo en este momento."}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Timeline de notificaciones */}
|
||||
{notifications.length === 0 ? (
|
||||
<View style={styles.emptyBox}>
|
||||
<Ionicons name="leaf-outline" size={40} color="#9CA3AF" />
|
||||
<Text style={styles.emptyText}>
|
||||
Aún no hay alertas. Las recibirás cuando el camión avance en
|
||||
tu ruta.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ paddingHorizontal: 20, marginTop: 12 }}>
|
||||
{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 (
|
||||
<View key={n.id} style={styles.timelineRow}>
|
||||
{/* Columna izquierda: dot + línea */}
|
||||
<View style={styles.timelineLeft}>
|
||||
<View
|
||||
style={[
|
||||
styles.timelineDot,
|
||||
{ backgroundColor: m.color },
|
||||
]}
|
||||
>
|
||||
<Ionicons name={m.icon} size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
{!isLast && (
|
||||
<View
|
||||
style={[
|
||||
styles.timelineLine,
|
||||
{ backgroundColor: `${m.color}50` },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Text style={styles.timelineTime}>{timeStr}</Text>
|
||||
</View>
|
||||
|
||||
{/* Card */}
|
||||
<View
|
||||
style={[
|
||||
styles.notifCard,
|
||||
{ backgroundColor: m.bg },
|
||||
]}
|
||||
>
|
||||
<View style={styles.notifHeader}>
|
||||
<Text style={styles.notifTitle}>{n.title}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: `${m.color}25` },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: m.color }]}>
|
||||
{m.badge}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.notifBody}>{n.body}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Consejo del día */}
|
||||
<View style={styles.tipCard}>
|
||||
<View style={styles.tipIcon}>
|
||||
<Ionicons name="leaf" size={20} color="#0E8A61" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.tipTitle}>Consejo del día</Text>
|
||||
<Text style={styles.tipBody}>
|
||||
Separa correctamente tus residuos. Tu acción hace la diferencia.
|
||||
</Text>
|
||||
</View>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/bins-cute.png")}
|
||||
style={styles.tipImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: COLORS.background }}>
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||
edges={["top"]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 120 }}
|
||||
>
|
||||
<SectionTitle title="Guía de separación" />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#6B7280",
|
||||
fontSize: 14,
|
||||
marginBottom: 20,
|
||||
marginLeft: 12,
|
||||
}}
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 130 }}
|
||||
>
|
||||
{/* Header con título grande + imagen */}
|
||||
<View style={styles.headerRow}>
|
||||
<View style={{ flex: 1, paddingRight: 8 }}>
|
||||
<Text style={styles.headerTitle}>Guía de{"\n"}separación</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Separar correctamente reduce contaminación y ayuda a la ruta de
|
||||
recolección. Funciona sin conexión.
|
||||
recolección.
|
||||
</Text>
|
||||
<Text style={styles.headerOffline}>Funciona sin conexión.</Text>
|
||||
</View>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/recycling.png")}
|
||||
style={styles.heroImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{CATEGORIES.map((cat) => (
|
||||
<View key={cat.key} style={styles.card}>
|
||||
{/* Icono circular */}
|
||||
<View
|
||||
style={[
|
||||
styles.iconWrap,
|
||||
{ backgroundColor: `${cat.color}20` },
|
||||
{ backgroundColor: cat.bg },
|
||||
]}
|
||||
>
|
||||
<Ionicons name={cat.icon} size={28} color={cat.color} />
|
||||
</View>
|
||||
|
||||
{/* Contenido */}
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.cardTitleRow}>
|
||||
<Text style={[styles.cardTitle, { color: cat.color }]}>
|
||||
{cat.title}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#9CA3AF" />
|
||||
</View>
|
||||
{cat.examples.map((ex, i) => (
|
||||
<Text key={i} style={styles.example}>
|
||||
• {ex}
|
||||
@@ -109,24 +130,50 @@ export default function GuideScreen() {
|
||||
<View
|
||||
style={[
|
||||
styles.tipBox,
|
||||
{ backgroundColor: `${cat.color}15` },
|
||||
{ backgroundColor: cat.bg },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="bulb" size={14} color={cat.color} />
|
||||
<Text style={[styles.tipText, { color: cat.color }]}>
|
||||
💡 {cat.tip}
|
||||
{cat.tip}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Imagen ilustrativa por categoría */}
|
||||
<Image
|
||||
source={cat.image}
|
||||
style={styles.categoryImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Recuerda */}
|
||||
<View style={styles.preventiveBox}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.preventiveHeader}>
|
||||
<View style={styles.preventiveIcon}>
|
||||
<Ionicons name="notifications" size={20} color="#FFFFFF" />
|
||||
</View>
|
||||
<Text style={styles.preventiveTitle}>Recuerda</Text>
|
||||
<Text style={styles.preventiveBody}>
|
||||
• 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.
|
||||
</Text>
|
||||
</View>
|
||||
{[
|
||||
"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) => (
|
||||
<View key={i} style={styles.preventiveRow}>
|
||||
<Ionicons name="checkmark-circle" size={14} color="#92400E" />
|
||||
<Text style={styles.preventiveBody}>{t}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/truck-banner.png")}
|
||||
style={styles.preventiveImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 <Redirect href="/login" />;
|
||||
useEffect(() => {
|
||||
if (user && !eta) {
|
||||
void refreshStatus();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const minutes = eta ? Math.max(0, eta.etaMinutes) : 0;
|
||||
if (!user) return <Redirect href="/login" />;
|
||||
|
||||
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 (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
}}
|
||||
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||
edges={["top"]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
paddingBottom: 120,
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={refreshStatus} />
|
||||
}
|
||||
contentContainerStyle={{ paddingBottom: 130 }}
|
||||
refreshControl={undefined}
|
||||
>
|
||||
<SectionTitle title="EcoRuta" />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#6B7280",
|
||||
fontSize: 14,
|
||||
marginBottom: 20,
|
||||
marginLeft: 12,
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
|
||||
<EtaCard minutes={minutes} status={windowText} />
|
||||
|
||||
{route?.horarioEstimado && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#ECFDF5",
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
marginHorizontal: 15,
|
||||
marginTop: 4,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: "#0E8A61",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#065F46", fontSize: 11, fontWeight: "700" }}>
|
||||
PRÓXIMA RECOLECCIÓN
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: "#065F46",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{route.horarioEstimado}
|
||||
{/* Title */}
|
||||
<View style={styles.titleRow}>
|
||||
<View>
|
||||
<Text style={styles.title}>EcoRuta</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Ruta {route?.routeId ?? "—"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FEF3C7",
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 15,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#92400E", fontSize: 12, fontWeight: "600" }}>
|
||||
⚠️ No saques tus residuos fuera del horario y no persigas al camión.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<SectionTitle title="Acciones rápidas" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<QuickAction
|
||||
title="Alertas"
|
||||
icon="notifications-outline"
|
||||
onPress={() => router.push("/alerts")}
|
||||
{/* Hero del camión */}
|
||||
<View style={styles.heroBox}>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/truck-city.png")}
|
||||
style={styles.heroTruck}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<QuickAction
|
||||
title="Reportar"
|
||||
icon="chatbubble-outline"
|
||||
onPress={() => router.push("/feedback")}
|
||||
{/* Card ETA */}
|
||||
<View style={styles.etaCard}>
|
||||
<View style={styles.etaContent}>
|
||||
<View style={styles.etaIcon}>
|
||||
<Ionicons name="time-outline" size={22} color="#0E8A61" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.etaLabel}>LLEGADA ESTIMADA</Text>
|
||||
<Text style={styles.etaMinutes}>
|
||||
{minutes !== null
|
||||
? `${minutes} min`
|
||||
: loading
|
||||
? "..."
|
||||
: "—"}
|
||||
</Text>
|
||||
<Text style={styles.etaWindowLabel}>Ventana de recolección</Text>
|
||||
<View style={styles.windowPill}>
|
||||
<Text style={styles.windowText}>{windowText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.etaCircle}>
|
||||
<View style={styles.etaCircleInner}>
|
||||
<Ionicons name="bus-outline" size={28} color="#0E8A61" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Estado de ruta */}
|
||||
<View style={styles.statusBanner}>
|
||||
<View
|
||||
style={[
|
||||
styles.statusDot,
|
||||
{ backgroundColor: statusColor },
|
||||
]}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.statusTitle, { color: statusColor }]}>
|
||||
ESTADO DE RUTA: {statusLabel}
|
||||
</Text>
|
||||
<Text style={styles.statusBody}>
|
||||
{statusOK
|
||||
? "Tu recolección sigue en horario normal."
|
||||
: "Hay incidencias en la ruta."}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.statusShield,
|
||||
{ backgroundColor: `${statusColor}15` },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name="shield-checkmark-outline"
|
||||
size={20}
|
||||
color={statusColor}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Mensaje preventivo */}
|
||||
<View style={styles.warningBanner}>
|
||||
<View style={styles.warningIcon}>
|
||||
<Ionicons name="alert" size={20} color="#FFFFFF" />
|
||||
</View>
|
||||
<Text style={styles.warningText}>
|
||||
No saques tus residuos fuera del horario{"\n"}y no persigas al camión.
|
||||
</Text>
|
||||
<View style={styles.warningRight}>
|
||||
<Ionicons name="walk-outline" size={20} color="#92400E" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Acciones rápidas */}
|
||||
<Text style={styles.sectionTitle}>ACCIONES RÁPIDAS</Text>
|
||||
<View style={styles.actionsGrid}>
|
||||
{quickActions.map((a) => (
|
||||
<Pressable
|
||||
key={a.key}
|
||||
style={styles.actionCard}
|
||||
onPress={a.onPress}
|
||||
>
|
||||
<View style={[styles.actionIcon, { backgroundColor: a.bg }]}>
|
||||
<Ionicons name={a.icon} size={26} color={a.color} />
|
||||
</View>
|
||||
<Text style={styles.actionTitle}>{a.title}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Consejo rápido */}
|
||||
<View style={styles.tipCard}>
|
||||
<View style={styles.tipIcon}>
|
||||
<Ionicons name="leaf" size={22} color="#0E8A61" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.tipTitle}>Consejo rápido</Text>
|
||||
<Text style={styles.tipBody}>
|
||||
Compacta cartón y PET para ahorrar espacio y ayudar al medio
|
||||
ambiente.
|
||||
</Text>
|
||||
</View>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/pet-bottles.png")}
|
||||
style={styles.tipImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { user, logout, refreshStatus } = useApp();
|
||||
const router = useRouter();
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
type Row = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
color: string;
|
||||
onPress: () => void;
|
||||
danger?: boolean;
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>No has iniciado sesión</Text>
|
||||
<PrimaryButton
|
||||
title="Iniciar sesión"
|
||||
onPress={() => router.push("/login")}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
export default function ProfileScreen() {
|
||||
const { user, logout } = useApp();
|
||||
const router = useRouter();
|
||||
const [myAddress, setMyAddress] = useState<MyAddress | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
void getMyAddress().then(setMyAddress).catch(() => {});
|
||||
}, [user]);
|
||||
|
||||
if (!user) return <Redirect href="/login" />;
|
||||
|
||||
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 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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Hola, {user.name}</Text>
|
||||
<Text style={styles.email}>{user.email}</Text>
|
||||
|
||||
<View style={{ height: 24 }} />
|
||||
|
||||
<PrimaryButton
|
||||
title="Mi domicilio"
|
||||
onPress={() => router.push("/addresses")}
|
||||
/>
|
||||
|
||||
<View style={{ height: 12 }} />
|
||||
|
||||
<PrimaryButton
|
||||
title="Buzón de retroalimentación"
|
||||
onPress={() => router.push("/feedback")}
|
||||
/>
|
||||
|
||||
<View style={{ height: 12 }} />
|
||||
|
||||
<PrimaryButton
|
||||
title="Reiniciar demo"
|
||||
onPress={handleResetDemo}
|
||||
/>
|
||||
|
||||
<View style={{ height: 12 }} />
|
||||
|
||||
<PrimaryButton title="Cerrar sesión" onPress={handleLogout} />
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||
edges={["bottom"]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 130 }}
|
||||
>
|
||||
{/* Header verde con avatar */}
|
||||
<View style={styles.heroWrap}>
|
||||
<View style={styles.heroBackground} />
|
||||
<View style={styles.avatarOuter}>
|
||||
<View style={styles.avatarInner}>
|
||||
<Text style={styles.avatarText}>{initial}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.identityBox}>
|
||||
<Text style={styles.name}>
|
||||
Hola, {user.name} <Text style={{ color: "#22C55E" }}>🌿</Text>
|
||||
</Text>
|
||||
<View style={styles.emailRow}>
|
||||
<Ionicons name="mail-outline" size={14} color="#6B7280" />
|
||||
<Text style={styles.email}>{user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Zona + ruta */}
|
||||
<Pressable
|
||||
style={styles.zoneRow}
|
||||
onPress={() => router.push("/addresses")}
|
||||
>
|
||||
<View style={styles.zoneCol}>
|
||||
<Ionicons name="location-outline" size={22} color="#0E8A61" />
|
||||
<View style={{ marginLeft: 8, flex: 1 }}>
|
||||
<Text style={styles.zoneLabel}>Tu zona</Text>
|
||||
<Text style={styles.zoneValue} numberOfLines={2}>
|
||||
{myAddress?.colonia ?? "Sin asignar"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.zoneDivider} />
|
||||
<View style={styles.zoneCol}>
|
||||
<Ionicons name="bus-outline" size={22} color="#0E8A61" />
|
||||
<View style={{ marginLeft: 8, flex: 1 }}>
|
||||
<Text style={styles.zoneLabel}>Ruta asignada</Text>
|
||||
<Text style={styles.zoneValue} numberOfLines={1}>
|
||||
{myAddress?.routeId ?? "—"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={18}
|
||||
color="#9CA3AF"
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{/* Lista de opciones */}
|
||||
<View style={{ paddingHorizontal: 16, marginTop: 4 }}>
|
||||
{rows.map((r) => (
|
||||
<Pressable key={r.key} style={styles.rowCard} onPress={r.onPress}>
|
||||
<View
|
||||
style={[
|
||||
styles.rowIcon,
|
||||
{ backgroundColor: `${r.color}15` },
|
||||
]}
|
||||
>
|
||||
<Ionicons name={r.icon} size={22} color={r.color} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={[
|
||||
styles.rowTitle,
|
||||
r.danger && { color: "#EF4444" },
|
||||
]}
|
||||
>
|
||||
{r.title}
|
||||
</Text>
|
||||
<Text style={styles.rowSubtitle}>{r.subtitle}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#9CA3AF" />
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Tu impacto cuenta */}
|
||||
<View style={styles.impactCard}>
|
||||
<View style={styles.impactLeaf}>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/impact-leaf.png")}
|
||||
style={{ width: 40, height: 40 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.impactTitle}>Tu impacto cuenta</Text>
|
||||
<Text style={styles.impactBody}>
|
||||
Siguiendo los horarios y separando correctamente, haces tu ciudad
|
||||
más limpia.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
heroWrap: {
|
||||
height: 140,
|
||||
backgroundColor: "#D1FAE5",
|
||||
overflow: "visible",
|
||||
marginBottom: 60,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 24,
|
||||
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,
|
||||
},
|
||||
title: {
|
||||
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: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 6,
|
||||
fontWeight: "800",
|
||||
color: "#0F172A",
|
||||
},
|
||||
emailRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 6,
|
||||
},
|
||||
email: {
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
color: "#6B7280",
|
||||
textAlign: "center",
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
export const API_URL = "http://10.0.2.2:8080";
|
||||
//export const API_URL = "http://172.20.10.4:8080";
|
||||