Files
hackathon-opti-1a67c9077937…/frontend/src/app/alerts.tsx
2026-05-23 05:00:54 -06:00

393 lines
9.8 KiB
TypeScript

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 { useApp } from "../context/AppContext";
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, route } = useApp();
if (!user) return <Redirect href="/login" />;
const statusInfo =
STATUS_LABEL[route?.status ?? "ESPERA"] ?? STATUS_LABEL.ESPERA;
return (
<SafeAreaView
style={{ flex: 1, backgroundColor: COLORS.background }}
edges={["top"]}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 130 }}
>
{/* 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>
</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,
},
});