feat: Improve the administrator profile
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { Tabs } from "expo-router";
|
import { Tabs } from "expo-router";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { AppProvider } from "../context/AppContext";
|
import { AppProvider, useApp } from "../context/AppContext";
|
||||||
import NotificationToast from "../components/NotificationToast";
|
import NotificationToast from "../components/NotificationToast";
|
||||||
|
|
||||||
export default function Layout() {
|
function AppTabs() {
|
||||||
|
const { user } = useApp();
|
||||||
|
const isAdmin = user?.role === "ADMIN";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProvider>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<NotificationToast />
|
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -21,26 +21,15 @@ export default function Layout() {
|
|||||||
bottom: 5,
|
bottom: 5,
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
|
|
||||||
height: 82,
|
height: 82,
|
||||||
|
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
|
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
|
|
||||||
borderTopWidth: 0,
|
borderTopWidth: 0,
|
||||||
|
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {
|
shadowOffset: { width: 0, height: 10 },
|
||||||
width: 0,
|
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
|
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 10,
|
shadowRadius: 10,
|
||||||
|
|
||||||
elevation: 10,
|
elevation: 10,
|
||||||
|
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -59,10 +48,12 @@ export default function Layout() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Tabs de USER — escondidas para ADMIN */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: "Inicio",
|
title: "Inicio",
|
||||||
|
href: isAdmin ? null : undefined,
|
||||||
tabBarIcon: ({ focused, color }) => (
|
tabBarIcon: ({ focused, color }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={focused ? "home" : "home-outline"}
|
name={focused ? "home" : "home-outline"}
|
||||||
@@ -77,6 +68,7 @@ export default function Layout() {
|
|||||||
name="alerts"
|
name="alerts"
|
||||||
options={{
|
options={{
|
||||||
title: "Alertas",
|
title: "Alertas",
|
||||||
|
href: isAdmin ? null : undefined,
|
||||||
tabBarIcon: ({ focused, color }) => (
|
tabBarIcon: ({ focused, color }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={focused ? "notifications" : "notifications-outline"}
|
name={focused ? "notifications" : "notifications-outline"}
|
||||||
@@ -91,6 +83,7 @@ export default function Layout() {
|
|||||||
name="guide"
|
name="guide"
|
||||||
options={{
|
options={{
|
||||||
title: "Guía",
|
title: "Guía",
|
||||||
|
href: isAdmin ? null : undefined,
|
||||||
tabBarIcon: ({ focused, color }) => (
|
tabBarIcon: ({ focused, color }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={focused ? "leaf" : "leaf-outline"}
|
name={focused ? "leaf" : "leaf-outline"}
|
||||||
@@ -105,6 +98,7 @@ export default function Layout() {
|
|||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
title: "Perfil",
|
title: "Perfil",
|
||||||
|
href: isAdmin ? null : undefined,
|
||||||
tabBarIcon: ({ focused, color }) => (
|
tabBarIcon: ({ focused, color }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={focused ? "person" : "person-outline"}
|
name={focused ? "person" : "person-outline"}
|
||||||
@@ -115,26 +109,37 @@ export default function Layout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Tab de ADMIN — solo visible si el user logueado es admin */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="login"
|
name="admin"
|
||||||
options={{ href: null }}
|
options={{
|
||||||
|
title: "Admin",
|
||||||
|
href: isAdmin ? undefined : null,
|
||||||
|
tabBarIcon: ({ focused, color }) => (
|
||||||
|
<Ionicons
|
||||||
|
name={focused ? "shield-checkmark" : "shield-checkmark-outline"}
|
||||||
|
size={focused ? 28 : 24}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs.Screen
|
{/* Pantallas siempre escondidas del tab bar */}
|
||||||
name="register"
|
<Tabs.Screen name="login" options={{ href: null }} />
|
||||||
options={{ href: null }}
|
<Tabs.Screen name="register" options={{ href: null }} />
|
||||||
/>
|
<Tabs.Screen name="feedback" options={{ href: null }} />
|
||||||
|
<Tabs.Screen name="addresses" options={{ href: null }} />
|
||||||
<Tabs.Screen
|
|
||||||
name="feedback"
|
|
||||||
options={{ href: null }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabs.Screen
|
|
||||||
name="addresses"
|
|
||||||
options={{ href: null }}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<AppProvider>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<NotificationToast />
|
||||||
|
<AppTabs />
|
||||||
</View>
|
</View>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
375
frontend/src/app/admin.tsx
Normal file
375
frontend/src/app/admin.tsx
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/**
|
||||||
|
* admin.tsx
|
||||||
|
*
|
||||||
|
* Panel de administración: lista las rutas y permite cancelarlas /
|
||||||
|
* reanudarlas. Solo accesible para usuarios con role=ADMIN.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
ScrollView,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
RefreshControl,
|
||||||
|
} 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 {
|
||||||
|
cancelRoute,
|
||||||
|
listAllRoutes,
|
||||||
|
resumeRoute,
|
||||||
|
type AdminRouteItem,
|
||||||
|
} from "../services/admin.service";
|
||||||
|
|
||||||
|
const arrivalLabel: Record<string, { label: string; color: string }> = {
|
||||||
|
PENDING: { label: "Pendiente", color: "#9CA3AF" },
|
||||||
|
ARRIVED: { label: "Llegó", color: "#22C55E" },
|
||||||
|
FAILED: { label: "Falla", color: "#EF4444" },
|
||||||
|
CANCELLED: { label: "Cancelada", color: "#F59E0B" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminScreen() {
|
||||||
|
const { user, logout } = useApp();
|
||||||
|
const [routes, setRoutes] = useState<AdminRouteItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchRoutes = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await listAllRoutes();
|
||||||
|
setRoutes(data);
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert(
|
||||||
|
"Error",
|
||||||
|
err instanceof Error ? err.message : "No se pudieron cargar las rutas",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user || user.role !== "ADMIN") return;
|
||||||
|
void fetchRoutes();
|
||||||
|
const interval = setInterval(() => void fetchRoutes(), 15_000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [user, fetchRoutes]);
|
||||||
|
|
||||||
|
if (!user) return <Redirect href="/login" />;
|
||||||
|
if (user.role !== "ADMIN") return <Redirect href="/" />;
|
||||||
|
|
||||||
|
const handleCancel = (routeId: string) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Cancelar ruta",
|
||||||
|
`¿Cancelar ${routeId}? Los usuarios suscritos serán notificados.`,
|
||||||
|
[
|
||||||
|
{ text: "No", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Sí, cancelar",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await cancelRoute(routeId, "Cancelada por administración");
|
||||||
|
void fetchRoutes();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert(
|
||||||
|
"Error",
|
||||||
|
err instanceof Error ? err.message : "No se pudo cancelar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResume = (routeId: string) => {
|
||||||
|
Alert.alert("Reanudar ruta", `¿Reactivar ${routeId}?`, [
|
||||||
|
{ text: "No", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Sí",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await resumeRoute(routeId);
|
||||||
|
void fetchRoutes();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert(
|
||||||
|
"Error",
|
||||||
|
err instanceof Error ? err.message : "No se pudo reanudar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView
|
||||||
|
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||||
|
edges={["top"]}
|
||||||
|
>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.title}>Panel de Administración</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
{user.email} · {routes.length} rutas
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable onPress={logout} style={styles.logoutBtn} hitSlop={10}>
|
||||||
|
<Ionicons name="log-out-outline" size={22} color="#EF4444" />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color="#0E8A61" style={{ marginTop: 40 }} />
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{ padding: 16, paddingBottom: 130 }}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={loading} onRefresh={fetchRoutes} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* === Reportes del día === */}
|
||||||
|
<Text style={styles.sectionTitle}>REPORTES DEL DÍA</Text>
|
||||||
|
<View style={styles.reportsGrid}>
|
||||||
|
{(() => {
|
||||||
|
const arrived = routes.filter(
|
||||||
|
(r) => r.arrivalResult === "ARRIVED",
|
||||||
|
).length;
|
||||||
|
const failed = routes.filter(
|
||||||
|
(r) => r.arrivalResult === "FAILED",
|
||||||
|
).length;
|
||||||
|
const cancelled = routes.filter(
|
||||||
|
(r) => r.cancelled || r.arrivalResult === "CANCELLED",
|
||||||
|
).length;
|
||||||
|
const pending = routes.filter(
|
||||||
|
(r) => r.arrivalResult === "PENDING" && !r.cancelled,
|
||||||
|
).length;
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: "arrived",
|
||||||
|
label: "Llegaron",
|
||||||
|
value: arrived,
|
||||||
|
icon: "checkmark-circle" as const,
|
||||||
|
color: "#22C55E",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "pending",
|
||||||
|
label: "En curso",
|
||||||
|
value: pending,
|
||||||
|
icon: "time-outline" as const,
|
||||||
|
color: "#3B82F6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "failed",
|
||||||
|
label: "Con falla",
|
||||||
|
value: failed,
|
||||||
|
icon: "warning" as const,
|
||||||
|
color: "#EF4444",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cancelled",
|
||||||
|
label: "Canceladas",
|
||||||
|
value: cancelled,
|
||||||
|
icon: "close-circle" as const,
|
||||||
|
color: "#F59E0B",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return items.map((it) => (
|
||||||
|
<View key={it.key} style={styles.reportCard}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.reportIcon,
|
||||||
|
{ backgroundColor: `${it.color}20` },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons name={it.icon} size={22} color={it.color} />
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.reportValue, { color: it.color }]}>
|
||||||
|
{it.value}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.reportLabel}>{it.label}</Text>
|
||||||
|
</View>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* === Lista de rutas === */}
|
||||||
|
<Text style={styles.sectionTitle}>RUTAS</Text>
|
||||||
|
{routes.map((r) => {
|
||||||
|
const arrival = arrivalLabel[r.arrivalResult] ?? arrivalLabel.PENDING;
|
||||||
|
return (
|
||||||
|
<View key={r.routeId} style={styles.card}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.routeId}>{r.routeId}</Text>
|
||||||
|
<Text style={styles.routeName}>{r.name}</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.badge,
|
||||||
|
{ backgroundColor: `${arrival.color}25` },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[styles.badgeText, { color: arrival.color }]}
|
||||||
|
>
|
||||||
|
{arrival.label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<Text style={styles.metaText}>
|
||||||
|
Camión #{r.truckId} · Posición {r.currentPositionId}/8 ·{" "}
|
||||||
|
{r.status}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{r.cancelled ? (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.actionBtn, { backgroundColor: "#22C55E" }]}
|
||||||
|
onPress={() => handleResume(r.routeId)}
|
||||||
|
>
|
||||||
|
<Ionicons name="play" size={16} color="#FFFFFF" />
|
||||||
|
<Text style={styles.actionText}>Reanudar</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.actionBtn, { backgroundColor: "#EF4444" }]}
|
||||||
|
onPress={() => handleCancel(r.routeId)}
|
||||||
|
>
|
||||||
|
<Ionicons name="close-circle" size={16} color="#FFFFFF" />
|
||||||
|
<Text style={styles.actionText}>Cancelar ruta</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{r.cancelReason ? (
|
||||||
|
<Text style={styles.reasonText}>
|
||||||
|
Motivo: {r.cancelReason}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
},
|
||||||
|
title: { fontSize: 24, fontWeight: "800", color: "#0F172A" },
|
||||||
|
subtitle: { fontSize: 12, color: "#6B7280", marginTop: 2 },
|
||||||
|
logoutBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: "#FEF2F2",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 14,
|
||||||
|
marginBottom: 12,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 6,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
routeId: { fontSize: 16, fontWeight: "800", color: "#0F172A" },
|
||||||
|
routeName: { fontSize: 12, color: "#6B7280", marginTop: 2 },
|
||||||
|
badge: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
badgeText: { fontSize: 11, fontWeight: "700" },
|
||||||
|
metaRow: { marginBottom: 12 },
|
||||||
|
metaText: { fontSize: 12, color: "#374151" },
|
||||||
|
actionBtn: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
actionText: {
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 13,
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
reasonText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#F59E0B",
|
||||||
|
marginTop: 8,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "800",
|
||||||
|
color: "#6B7280",
|
||||||
|
letterSpacing: 1,
|
||||||
|
marginBottom: 10,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
reportsGrid: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 18,
|
||||||
|
},
|
||||||
|
reportCard: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 10,
|
||||||
|
marginHorizontal: 3,
|
||||||
|
alignItems: "center",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
reportIcon: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
reportValue: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: "800",
|
||||||
|
},
|
||||||
|
reportLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#374151",
|
||||||
|
marginTop: 2,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -34,22 +34,47 @@ export default function HomeScreen() {
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
if (!user) return <Redirect href="/login" />;
|
if (!user) return <Redirect href="/login" />;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
if (user.role === "ADMIN") return <Redirect href={"/admin" as any} />;
|
||||||
|
|
||||||
const minutes = eta ? Math.max(0, eta.etaMinutes) : null;
|
const minutes = eta ? Math.max(0, eta.etaMinutes) : null;
|
||||||
const windowText = eta?.arrivalWindow
|
const windowText = eta?.arrivalWindow
|
||||||
? `${eta.arrivalWindow.from} - ${eta.arrivalWindow.to} a.m.`
|
? `${eta.arrivalWindow.from} - ${eta.arrivalWindow.to} a.m.`
|
||||||
: "Calculando...";
|
: "Calculando...";
|
||||||
|
|
||||||
const statusOK = route?.status === "EN_RUTA" || route?.status === "ESPERA";
|
// Si la ruta fue cancelada por admin → eso manda
|
||||||
const statusColor = statusOK ? "#22C55E" : "#EF4444";
|
// Si el simulador marcó FAILED → falla mecánica
|
||||||
const statusLabel =
|
// Si el simulador marcó ARRIVED → ya pasó el camión
|
||||||
route?.status === "EN_RUTA"
|
// Si no, mostramos el status genérico
|
||||||
? "ESTABLE"
|
const arrival = route?.arrivalResult ?? "PENDING";
|
||||||
: route?.status === "FALLA"
|
const cancelled = route?.cancelled ?? false;
|
||||||
|
|
||||||
|
const statusOK = !cancelled && arrival !== "FAILED";
|
||||||
|
const statusColor = cancelled
|
||||||
|
? "#F59E0B"
|
||||||
|
: arrival === "FAILED"
|
||||||
|
? "#EF4444"
|
||||||
|
: arrival === "ARRIVED"
|
||||||
|
? "#22C55E"
|
||||||
|
: "#22C55E";
|
||||||
|
const statusLabel = cancelled
|
||||||
|
? "CANCELADA"
|
||||||
|
: arrival === "FAILED"
|
||||||
? "CON FALLA"
|
? "CON FALLA"
|
||||||
: route?.status === "DETENIDO"
|
: arrival === "ARRIVED"
|
||||||
? "DETENIDO"
|
? "EL CAMIÓN YA PASÓ"
|
||||||
|
: route?.status === "EN_RUTA"
|
||||||
|
? "ESTABLE"
|
||||||
: "EN ESPERA";
|
: "EN ESPERA";
|
||||||
|
const statusBody = cancelled
|
||||||
|
? "La ruta matutina fue cancelada. Se reprogramará para la tarde."
|
||||||
|
: arrival === "FAILED"
|
||||||
|
? "El camión presenta una falla mecánica."
|
||||||
|
: arrival === "ARRIVED"
|
||||||
|
? "El camión recolector ya pasó por tu zona."
|
||||||
|
: statusOK
|
||||||
|
? "Tu recolección sigue en horario normal."
|
||||||
|
: "Hay incidencias en la ruta.";
|
||||||
|
|
||||||
const quickActions: QuickAction[] = [
|
const quickActions: QuickAction[] = [
|
||||||
{
|
{
|
||||||
@@ -155,11 +180,7 @@ export default function HomeScreen() {
|
|||||||
<Text style={[styles.statusTitle, { color: statusColor }]}>
|
<Text style={[styles.statusTitle, { color: statusColor }]}>
|
||||||
ESTADO DE RUTA: {statusLabel}
|
ESTADO DE RUTA: {statusLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statusBody}>
|
<Text style={styles.statusBody}>{statusBody}</Text>
|
||||||
{statusOK
|
|
||||||
? "Tu recolección sigue en horario normal."
|
|
||||||
: "Hay incidencias en la ruta."}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// android
|
// android
|
||||||
export const API_URL = "http://10.0.2.2:8080";
|
// export const API_URL = "http://10.0.2.2:8080";
|
||||||
//export const API_URL = "http://172.20.10.4:8080";
|
export const API_URL = "http://172.20.10.4:8080";
|
||||||
@@ -135,9 +135,10 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
initialRefreshDoneRef.current = false;
|
initialRefreshDoneRef.current = false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Polling controlado: arranca al loguearse, se detiene al deslogearse.
|
// Polling controlado: arranca al loguearse como user normal, se detiene
|
||||||
|
// al deslogearse o si el usuario es admin (los admins no usan /status).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user || user.role === "ADMIN") {
|
||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
clearInterval(pollingRef.current);
|
clearInterval(pollingRef.current);
|
||||||
pollingRef.current = null;
|
pollingRef.current = null;
|
||||||
|
|||||||
34
frontend/src/services/admin.service.ts
Normal file
34
frontend/src/services/admin.service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { apiFetch } from "../lib/api";
|
||||||
|
|
||||||
|
export interface AdminRouteItem {
|
||||||
|
routeId: string;
|
||||||
|
name: string;
|
||||||
|
truckId: number;
|
||||||
|
status: string;
|
||||||
|
currentPositionId: number;
|
||||||
|
arrivalResult: "PENDING" | "ARRIVED" | "FAILED" | "CANCELLED";
|
||||||
|
cancelled: boolean;
|
||||||
|
cancelReason?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listAllRoutes = () =>
|
||||||
|
apiFetch<AdminRouteItem[]>("/api/admin/routes");
|
||||||
|
|
||||||
|
export const cancelRoute = (routeId: string, reason?: string) =>
|
||||||
|
apiFetch<{ message: string; routeId: string }>(
|
||||||
|
`/api/admin/routes/${routeId}/cancel`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(reason ? { reason } : {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resumeRoute = (routeId: string) =>
|
||||||
|
apiFetch<{ message: string; routeId: string }>(
|
||||||
|
`/api/admin/routes/${routeId}/resume`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -4,6 +4,7 @@ export interface AuthUser {
|
|||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const register = (data: { name: string; email: string; password: string }) =>
|
export const register = (data: { name: string; email: string; password: string }) =>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export interface InboxNotification {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ArrivalResult = "PENDING" | "ARRIVED" | "FAILED" | "CANCELLED";
|
||||||
|
|
||||||
export interface UserStatusResponse {
|
export interface UserStatusResponse {
|
||||||
user: { id: number; name: string; colonia: string };
|
user: { id: number; name: string; colonia: string };
|
||||||
route: {
|
route: {
|
||||||
@@ -62,6 +64,8 @@ export interface UserStatusResponse {
|
|||||||
status: string;
|
status: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
horarioEstimado: string | null;
|
horarioEstimado: string | null;
|
||||||
|
arrivalResult: ArrivalResult;
|
||||||
|
cancelled: boolean;
|
||||||
};
|
};
|
||||||
eta: EtaResult | null;
|
eta: EtaResult | null;
|
||||||
notifications: InboxNotification[];
|
notifications: InboxNotification[];
|
||||||
|
|||||||
Reference in New Issue
Block a user