feat: add calendar
This commit is contained in:
@@ -3,6 +3,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
import { COLORS } from "../constants/colors";
|
import { COLORS } from "../constants/colors";
|
||||||
|
import CollectionCalendar from "../components/CollectionCalendar";
|
||||||
|
|
||||||
type Category = {
|
type Category = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -85,6 +86,9 @@ export default function GuideScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{ padding: 20, paddingBottom: 130 }}
|
contentContainerStyle={{ padding: 20, paddingBottom: 130 }}
|
||||||
>
|
>
|
||||||
|
{/* Calendario de recolección (primer elemento) */}
|
||||||
|
<CollectionCalendar />
|
||||||
|
|
||||||
{/* Header con título grande + imagen */}
|
{/* Header con título grande + imagen */}
|
||||||
<View style={styles.headerRow}>
|
<View style={styles.headerRow}>
|
||||||
<View style={{ flex: 1, paddingRight: 8 }}>
|
<View style={{ flex: 1, paddingRight: 8 }}>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default function ProfileScreen() {
|
|||||||
try {
|
try {
|
||||||
await apiFetch<{ message: string }>("/api/tracking/reset-demo", {
|
await apiFetch<{ message: string }>("/api/tracking/reset-demo", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
Alert.alert("Demo reiniciada", "Las notificaciones se borraron.");
|
Alert.alert("Demo reiniciada", "Las notificaciones se borraron.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
447
frontend/src/components/CollectionCalendar.tsx
Normal file
447
frontend/src/components/CollectionCalendar.tsx
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
/**
|
||||||
|
* CollectionCalendar.tsx
|
||||||
|
*
|
||||||
|
* Calendario mensual de recolección de la ruta ASIGNADA al usuario.
|
||||||
|
* Respeta el principio de "visión de túnel": el ciudadano NO puede ver
|
||||||
|
* rutas que no sean la suya.
|
||||||
|
*
|
||||||
|
* Reglas de color por día (solo para RUTA-40, RUTA-65 y RUTA-80):
|
||||||
|
* L, M, V, S → verde (RESIDUOS ORGÁNICOS)
|
||||||
|
* Mi, J → gris (RESIDUOS SÓLIDOS URBANOS VALORABLES)
|
||||||
|
* Domingo → vacío
|
||||||
|
*
|
||||||
|
* El patrón se aplica TODAS las semanas sin excepción. Las celdas del mes
|
||||||
|
* anterior/siguiente que aparecen al inicio/final también se colorean
|
||||||
|
* (en tono más claro) para que no queden huecos entre semana.
|
||||||
|
*
|
||||||
|
* Otras rutas: días neutros sin etiqueta.
|
||||||
|
*
|
||||||
|
* Si el usuario no ha validado su domicilio, se muestra un CTA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
|
import { useApp } from "../context/AppContext";
|
||||||
|
import { routes as routeCatalog } from "../data/mocks/routes.mock";
|
||||||
|
|
||||||
|
const MONTHS_ES = [
|
||||||
|
"Enero",
|
||||||
|
"Febrero",
|
||||||
|
"Marzo",
|
||||||
|
"Abril",
|
||||||
|
"Mayo",
|
||||||
|
"Junio",
|
||||||
|
"Julio",
|
||||||
|
"Agosto",
|
||||||
|
"Septiembre",
|
||||||
|
"Octubre",
|
||||||
|
"Noviembre",
|
||||||
|
"Diciembre",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DAYS_ES = ["Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"];
|
||||||
|
|
||||||
|
const SPECIAL_ROUTES = new Set(["RUTA-40", "RUTA-65", "RUTA-80"]);
|
||||||
|
|
||||||
|
const COLOR_ORGANIC = "#10b981";
|
||||||
|
const COLOR_SOLID = "#9ca3af";
|
||||||
|
const COLOR_NEUTRAL = "#E5E7EB";
|
||||||
|
const COLOR_EMPTY = "transparent";
|
||||||
|
|
||||||
|
type DayInfo = { day: number; weekIndex: number; inMonth: boolean };
|
||||||
|
|
||||||
|
/** Convierte JS getDay (0=Sun..6=Sat) al índice Mon-first (0=Lun..6=Dom). */
|
||||||
|
const toMondayFirst = (jsDay: number): number => (jsDay === 0 ? 6 : jsDay - 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve 42 celdas (6 semanas × 7 días). Las celdas del inicio/final
|
||||||
|
* que caen fuera del mes actual también traen su día real del mes vecino
|
||||||
|
* (con inMonth=false) para que el calendario no tenga huecos entre semana.
|
||||||
|
*/
|
||||||
|
function buildMonthGrid(year: number, month: number): DayInfo[] {
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const firstWeekIndex = toMondayFirst(firstDay.getDay());
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
const daysInPrevMonth = new Date(year, month, 0).getDate();
|
||||||
|
|
||||||
|
const cells: DayInfo[] = [];
|
||||||
|
|
||||||
|
// Días del mes anterior que aparecen al inicio
|
||||||
|
for (let i = firstWeekIndex - 1; i >= 0; i--) {
|
||||||
|
const date = new Date(year, month - 1, daysInPrevMonth - i);
|
||||||
|
cells.push({
|
||||||
|
day: daysInPrevMonth - i,
|
||||||
|
weekIndex: toMondayFirst(date.getDay()),
|
||||||
|
inMonth: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Días del mes actual
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const date = new Date(year, month, d);
|
||||||
|
cells.push({
|
||||||
|
day: d,
|
||||||
|
weekIndex: toMondayFirst(date.getDay()),
|
||||||
|
inMonth: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Días del mes siguiente para completar 42 celdas
|
||||||
|
let next = 1;
|
||||||
|
while (cells.length < 42) {
|
||||||
|
const date = new Date(year, month + 1, next);
|
||||||
|
cells.push({
|
||||||
|
day: next,
|
||||||
|
weekIndex: toMondayFirst(date.getDay()),
|
||||||
|
inMonth: false,
|
||||||
|
});
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color de fondo según ruta + día de la semana.
|
||||||
|
* - SPECIAL_ROUTES (40/65/80): patrón verde/gris/vacío todas las semanas
|
||||||
|
* - Otras rutas: neutro (salvo domingo que va vacío)
|
||||||
|
*/
|
||||||
|
function colorForDay(routeId: string, weekIndex: number): string {
|
||||||
|
if (!SPECIAL_ROUTES.has(routeId)) {
|
||||||
|
return weekIndex === 6 ? COLOR_EMPTY : COLOR_NEUTRAL;
|
||||||
|
}
|
||||||
|
if (weekIndex === 0 || weekIndex === 1 || weekIndex === 4 || weekIndex === 5)
|
||||||
|
return COLOR_ORGANIC; // Lun, Mar, Vie, Sáb
|
||||||
|
if (weekIndex === 2 || weekIndex === 3) return COLOR_SOLID; // Mié, Jue
|
||||||
|
return COLOR_EMPTY; // Domingo
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CollectionCalendar() {
|
||||||
|
const { route } = useApp();
|
||||||
|
const router = useRouter();
|
||||||
|
const today = new Date();
|
||||||
|
const [year, setYear] = useState(today.getFullYear());
|
||||||
|
const [month, setMonth] = useState(today.getMonth());
|
||||||
|
|
||||||
|
const myRouteId = route?.routeId ?? null;
|
||||||
|
const myRouteName = useMemo(
|
||||||
|
() => routeCatalog.find((r) => r.routeId === myRouteId)?.name ?? "",
|
||||||
|
[myRouteId],
|
||||||
|
);
|
||||||
|
const isSpecial = myRouteId ? SPECIAL_ROUTES.has(myRouteId) : false;
|
||||||
|
|
||||||
|
const grid = useMemo(() => buildMonthGrid(year, month), [year, month]);
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (month === 0) {
|
||||||
|
setMonth(11);
|
||||||
|
setYear((y) => y - 1);
|
||||||
|
} else {
|
||||||
|
setMonth((m) => m - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (month === 11) {
|
||||||
|
setMonth(0);
|
||||||
|
setYear((y) => y + 1);
|
||||||
|
} else {
|
||||||
|
setMonth((m) => m + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CTA: sin ruta asignada
|
||||||
|
if (!myRouteId) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<Ionicons name="calendar-outline" size={22} color="#0E8A61" />
|
||||||
|
<Text style={styles.title}>Calendario de recolección</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.ctaBox}>
|
||||||
|
<Ionicons name="lock-closed-outline" size={28} color="#9CA3AF" />
|
||||||
|
<Text style={styles.ctaText}>
|
||||||
|
Necesitas validar tu domicilio para ver el calendario de tu ruta.
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
style={styles.ctaBtn}
|
||||||
|
onPress={() => router.push("/addresses")}
|
||||||
|
>
|
||||||
|
<Text style={styles.ctaBtnText}>Validar domicilio</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<Ionicons name="calendar-outline" size={22} color="#0E8A61" />
|
||||||
|
<Text style={styles.title}>Calendario de recolección</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.routeBadge}>
|
||||||
|
<Ionicons name="bus-outline" size={14} color="#0E8A61" />
|
||||||
|
<Text style={styles.routeBadgeText}>
|
||||||
|
Tu ruta: <Text style={styles.routeBadgeBold}>{myRouteId}</Text>
|
||||||
|
{myRouteName ? ` · ${myRouteName}` : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Navegador de mes */}
|
||||||
|
<View style={styles.monthRow}>
|
||||||
|
<Pressable onPress={handlePrev} style={styles.navBtn} hitSlop={10}>
|
||||||
|
<Ionicons name="chevron-back" size={20} color="#0E8A61" />
|
||||||
|
</Pressable>
|
||||||
|
<Text style={styles.monthLabel}>
|
||||||
|
{MONTHS_ES[month]} {year}
|
||||||
|
</Text>
|
||||||
|
<Pressable onPress={handleNext} style={styles.navBtn} hitSlop={10}>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#0E8A61" />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Encabezado días de la semana */}
|
||||||
|
<View style={styles.weekHeader}>
|
||||||
|
{DAYS_ES.map((d) => (
|
||||||
|
<View key={d} style={styles.weekCell}>
|
||||||
|
<Text style={styles.weekText}>{d}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Grid de días — 6 filas explícitas para evitar el problema de
|
||||||
|
alineación que ocurre con flexWrap + porcentajes en React Native. */}
|
||||||
|
<View>
|
||||||
|
{Array.from({ length: 6 }).map((_, rowIdx) => (
|
||||||
|
<View key={rowIdx} style={styles.gridRow}>
|
||||||
|
{grid.slice(rowIdx * 7, (rowIdx + 1) * 7).map((cell, colIdx) => {
|
||||||
|
const bg = colorForDay(myRouteId, cell.weekIndex);
|
||||||
|
const isToday =
|
||||||
|
cell.inMonth &&
|
||||||
|
cell.day === today.getDate() &&
|
||||||
|
month === today.getMonth() &&
|
||||||
|
year === today.getFullYear();
|
||||||
|
const isColored =
|
||||||
|
bg === COLOR_ORGANIC || bg === COLOR_SOLID;
|
||||||
|
const textColor = isColored ? "#FFFFFF" : "#0F172A";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={colIdx} style={styles.dayCell}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.dayInner,
|
||||||
|
{ backgroundColor: bg },
|
||||||
|
!cell.inMonth && { opacity: 0.4 },
|
||||||
|
isToday && styles.todayBorder,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.dayText, { color: textColor }]}>
|
||||||
|
{cell.day}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Leyenda */}
|
||||||
|
{isSpecial ? (
|
||||||
|
<View style={styles.legend}>
|
||||||
|
<View style={styles.legendRow}>
|
||||||
|
<View
|
||||||
|
style={[styles.legendSwatch, { backgroundColor: COLOR_ORGANIC }]}
|
||||||
|
/>
|
||||||
|
<Text style={styles.legendText}>RESIDUOS ORGÁNICOS</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.legendRow}>
|
||||||
|
<View
|
||||||
|
style={[styles.legendSwatch, { backgroundColor: COLOR_SOLID }]}
|
||||||
|
/>
|
||||||
|
<Text style={styles.legendText}>
|
||||||
|
RESIDUOS SÓLIDOS URBANOS VALORABLES
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.legendNeutral}>
|
||||||
|
<Ionicons
|
||||||
|
name="information-circle-outline"
|
||||||
|
size={16}
|
||||||
|
color="#6B7280"
|
||||||
|
/>
|
||||||
|
<Text style={styles.legendNeutralText}>
|
||||||
|
Tu ruta aún no tiene un calendario de separación específico. Te
|
||||||
|
avisaremos cuando se publique.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 18,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "800",
|
||||||
|
color: "#0F172A",
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
routeBadge: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
backgroundColor: "#ECFDF5",
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 999,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
routeBadgeText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#065F46",
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
routeBadgeBold: {
|
||||||
|
fontWeight: "800",
|
||||||
|
color: "#0E8A61",
|
||||||
|
},
|
||||||
|
monthRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
navBtn: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: "#ECFDF5",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
monthLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#0F172A",
|
||||||
|
},
|
||||||
|
weekHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
weekCell: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 2,
|
||||||
|
},
|
||||||
|
weekText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#9CA3AF",
|
||||||
|
},
|
||||||
|
gridRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
|
dayCell: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 2,
|
||||||
|
aspectRatio: 1,
|
||||||
|
},
|
||||||
|
dayInner: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dayText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
todayBorder: {
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#0E8A61",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
marginTop: 12,
|
||||||
|
paddingTop: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
legendRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
legendSwatch: {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
legendText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#374151",
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
legendNeutral: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 12,
|
||||||
|
paddingTop: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
legendNeutralText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#6B7280",
|
||||||
|
marginLeft: 6,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
ctaBox: {
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
ctaText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#6B7280",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 10,
|
||||||
|
marginBottom: 14,
|
||||||
|
},
|
||||||
|
ctaBtn: {
|
||||||
|
backgroundColor: "#0E8A61",
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
ctaBtnText: {
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
});
|
||||||
32
frontend/src/data/mocks/routes.mock.ts
Normal file
32
frontend/src/data/mocks/routes.mock.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* routes.mock.ts (frontend)
|
||||||
|
* Lista de rutas para el calendario de recolección.
|
||||||
|
* No incluye coordenadas GPS — eso vive en el backend.
|
||||||
|
* Solo lo necesario para el selector y el calendario.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MockRoute {
|
||||||
|
routeId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routes: MockRoute[] = [
|
||||||
|
{ routeId: "RUTA-01", name: "Zona Centro - Las Arboledas" },
|
||||||
|
{ routeId: "RUTA-02", name: "Sector Norte - Av. Tecnológico" },
|
||||||
|
{ routeId: "RUTA-03", name: "Sector Poniente - San Juanico" },
|
||||||
|
{ routeId: "RUTA-04", name: "Oriente - Los Olivos" },
|
||||||
|
{ routeId: "RUTA-05", name: "Sector Sur - Rancho Seco" },
|
||||||
|
{ routeId: "RUTA-06", name: "Norte Extremo - Rumbos de Roque" },
|
||||||
|
{ routeId: "RUTA-07", name: "Nororiente - Ciudad Industrial" },
|
||||||
|
{ routeId: "RUTA-08", name: "Suroriente - Universidad Latina" },
|
||||||
|
{ routeId: "RUTA-09", name: "Poniente - Hospital General" },
|
||||||
|
{ routeId: "RUTA-10", name: "Eje Juan Pablo II" },
|
||||||
|
{ routeId: "RUTA-11", name: "Zona de Oro - Torres Landa" },
|
||||||
|
{ routeId: "RUTA-12", name: "Nororiente - Las Insurgentes" },
|
||||||
|
{ routeId: "RUTA-13", name: "Sector Norte - Trojes e Irrigación" },
|
||||||
|
{ routeId: "RUTA-14", name: "Sur Poniente - La Toscana" },
|
||||||
|
{ routeId: "RUTA-15", name: "Norponiente - San José de Celaya" },
|
||||||
|
{ routeId: "RUTA-40", name: "Norte Industrial" },
|
||||||
|
{ routeId: "RUTA-65", name: "Centro Comercial" },
|
||||||
|
{ routeId: "RUTA-80", name: "Sector Residencial Sur" },
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user