feat: Add feedback for administrator
This commit is contained in:
@@ -1,56 +1,293 @@
|
||||
# Welcome to your Expo app 👋
|
||||
# Frontend — OptiRuta
|
||||
|
||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||
App móvil en **Expo SDK 56 + React Native 0.85 + TypeScript** con **expo-router**.
|
||||
|
||||
## Get started
|
||||
> Para el README general, ver [../README.md](../README.md).
|
||||
> Para la guía de integración con el backend (compañeros), ver [INTEGRATION.md](INTEGRATION.md).
|
||||
|
||||
1. Install dependencies
|
||||
---
|
||||
|
||||
```bash
|
||||
## Tabla de contenidos
|
||||
|
||||
- [Stack](#stack)
|
||||
- [Estructura](#estructura)
|
||||
- [Cómo correr](#cómo-correr)
|
||||
- [Configurar la URL del backend](#configurar-la-url-del-backend)
|
||||
- [Pantallas](#pantallas)
|
||||
- [Estado global (AppContext)](#estado-global-appcontext)
|
||||
- [Comunicación con el backend](#comunicación-con-el-backend)
|
||||
- [Componentes reutilizables](#componentes-reutilizables)
|
||||
- [Roles y navegación](#roles-y-navegación)
|
||||
- [Notificaciones](#notificaciones)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Stack
|
||||
|
||||
- **Expo SDK 56** (Sept 2025)
|
||||
- **React Native 0.85** + **React 19**
|
||||
- **TypeScript** estricto
|
||||
- **expo-router 56** — navegación basada en archivos
|
||||
- **@expo/vector-icons** — iconos Ionicons
|
||||
- **expo-image** — imágenes optimizadas
|
||||
- Sin librerías de estado externas (solo Context)
|
||||
- Sin librerías de UI externas (StyleSheet nativo)
|
||||
|
||||
---
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app.json ← config de Expo (name="OptiRuta", slug, etc.)
|
||||
├── tsconfig.json ← paths "@/*" → "./src/*"
|
||||
├── assets/
|
||||
│ ├── images/ ← icons del SO, splash
|
||||
│ └── illustrations/ ← ilustraciones del UI (camión, botes, etc.)
|
||||
└── src/
|
||||
├── app/ ← rutas de expo-router (cada archivo = pantalla)
|
||||
│ ├── _layout.tsx ← Tabs + AppProvider + Toast
|
||||
│ ├── index.tsx ← Home (ETA + estado de ruta)
|
||||
│ ├── login.tsx
|
||||
│ ├── register.tsx
|
||||
│ ├── addresses.tsx ← validación de domicilio
|
||||
│ ├── alerts.tsx ← timeline de notificaciones
|
||||
│ ├── feedback.tsx ← buzón de retro
|
||||
│ ├── guide.tsx ← calendario + guía de separación
|
||||
│ ├── admin.tsx ← panel admin
|
||||
│ └── profile.tsx ← perfil + ajustes
|
||||
├── components/
|
||||
│ ├── EtaCard.tsx
|
||||
│ ├── Alertltem.tsx ← (sic) typo histórico, no romper
|
||||
│ ├── InputField.tsx
|
||||
│ ├── PrimaryButton.tsx
|
||||
│ ├── QuickAction.tsx
|
||||
│ ├── SectionTitle.tsx
|
||||
│ ├── StatusBadge.tsx
|
||||
│ ├── CollectionCalendar.tsx
|
||||
│ └── NotificationToast.tsx
|
||||
├── config/
|
||||
│ └── api.ts ← URL del backend (cambia según dispositivo)
|
||||
├── constants/
|
||||
│ ├── colors.ts
|
||||
│ └── theme.ts
|
||||
├── context/
|
||||
│ └── AppContext.tsx ← estado global + polling
|
||||
├── data/
|
||||
│ └── mocks/
|
||||
│ └── routes.mock.ts ← catálogo de routeId/name (para el calendario)
|
||||
├── hooks/
|
||||
├── lib/
|
||||
│ ├── api.ts ← apiFetch (wrapper de fetch + auth token)
|
||||
│ ├── notifications.ts ← (preparado para Dev Build; no se importa en Go)
|
||||
│ └── notification-mapper.ts
|
||||
└── services/
|
||||
├── auth.service.ts
|
||||
├── tracking.service.ts
|
||||
├── addresses.service.ts
|
||||
├── feedback.service.ts
|
||||
└── admin.service.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cómo correr
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install
|
||||
npx expo start -c # -c limpia caché de Metro
|
||||
```
|
||||
|
||||
2. Start the app
|
||||
Después:
|
||||
- `a` → Android emulator
|
||||
- `i` → iOS simulator (solo Mac)
|
||||
- `w` → web
|
||||
- Escanear QR con Expo Go en celular físico
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
---
|
||||
|
||||
## Configurar la URL del backend
|
||||
|
||||
Edita `src/config/api.ts`:
|
||||
|
||||
```ts
|
||||
// Para Android emulator local:
|
||||
export const API_URL = "http://10.0.2.2:8080";
|
||||
|
||||
// Para celular físico (mismo Wi-Fi o hotspot):
|
||||
// export const API_URL = "http://192.168.X.X:8080";
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in a
|
||||
| Entorno | URL |
|
||||
|---|---|
|
||||
| Android Studio emulator | `http://10.0.2.2:8080` |
|
||||
| iOS Simulator | `http://localhost:8080` |
|
||||
| Celular físico Android/iOS (mismo Wi-Fi) | `http://<IP-LAN>:8080` |
|
||||
| Celular físico (hotspot) | `http://<IP-LAN-hotspot>:8080` |
|
||||
| Web | `http://localhost:8080` |
|
||||
|
||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
> Si estás en red de escuela/oficina con AP isolation, usa hotspot del celular.
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
---
|
||||
|
||||
## Get a fresh project
|
||||
## Pantallas
|
||||
|
||||
When you're ready, run:
|
||||
| Ruta | Archivo | Rol | Descripción |
|
||||
|---|---|---|---|
|
||||
| `/` | `index.tsx` | USER | Home: título + hero del camión + card ETA + banner estado de ruta + banner preventivo + 4 acciones rápidas + consejo |
|
||||
| `/login` | `login.tsx` | público | Login con email + password |
|
||||
| `/register` | `register.tsx` | público | Registro con nombre, email, password ≥ 6 caracteres |
|
||||
| `/addresses` | `addresses.tsx` | USER | Lista de colonias + selector + guardar (PUT /me) |
|
||||
| `/alerts` | `alerts.tsx` | USER | Hero + estado de ruta + timeline vertical con dots de color por severidad |
|
||||
| `/guide` | `guide.tsx` | USER | Calendario semanal + 4 cards (Orgánicos, Reciclables, Sanitarios, Especiales) + Recuerda |
|
||||
| `/feedback` | `feedback.tsx` | USER | Tipo de mensaje (4 opciones) + estrellas si es rating + textarea |
|
||||
| `/profile` | `profile.tsx` | USER | Avatar circular + cards Tu zona/Ruta + opciones (Mi domicilio, Buzón, Reiniciar demo, Ayuda, Cerrar sesión) + Tu impacto |
|
||||
| `/admin` | `admin.tsx` | ADMIN | Reportes del día + lista de rutas + cancelar/reanudar |
|
||||
|
||||
```bash
|
||||
npm run reset-project
|
||||
---
|
||||
|
||||
## Estado global (AppContext)
|
||||
|
||||
`src/context/AppContext.tsx`
|
||||
|
||||
Proveedor único que expone:
|
||||
|
||||
```ts
|
||||
interface AppContextValue {
|
||||
user: AuthUser | null; // { id, name, email, role }
|
||||
eta: EtaResult | null;
|
||||
notifications: InboxNotification[];
|
||||
route: RouteState | null;
|
||||
loading: boolean;
|
||||
toast: InboxNotification | null;
|
||||
dismissToast: () => void;
|
||||
login: (email, password) => Promise<void>;
|
||||
register: (name, email, password) => Promise<void>;
|
||||
logout: () => void;
|
||||
refreshStatus: () => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
||||
### Polling
|
||||
|
||||
### Other setup steps
|
||||
- Arranca cuando `user` ≠ null Y `user.role` ≠ "ADMIN"
|
||||
- Cada **30 segundos** llama `GET /api/tracking/status`
|
||||
- Limpia el interval cuando el user cierra sesión
|
||||
- Detecta notificaciones nuevas (vía Set de IDs ya vistos) y dispara el `toast`
|
||||
|
||||
- To set up ESLint for linting, run `npx expo lint`, or follow our guide on ["Using ESLint and Prettier"](https://docs.expo.dev/guides/using-eslint/)
|
||||
- If you'd like to set up unit testing, follow our guide on ["Unit Testing with Jest"](https://docs.expo.dev/develop/unit-testing/)
|
||||
- Learn more about the TypeScript setup in this template in our guide on ["Using TypeScript"](https://docs.expo.dev/guides/typescript/)
|
||||
### Logout
|
||||
- Borra token con `setAuthToken(null)`
|
||||
- Resetea user, eta, route, notifications, toast
|
||||
- Limpia el Set de IDs ya vistos
|
||||
|
||||
## Learn more
|
||||
---
|
||||
|
||||
To learn more about developing your project with Expo, look at the following resources:
|
||||
## Comunicación con el backend
|
||||
|
||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
||||
### `lib/api.ts`
|
||||
|
||||
## Join the community
|
||||
```ts
|
||||
let authToken: string | null = null;
|
||||
|
||||
Join our community of developers creating universal apps.
|
||||
export const setAuthToken = (token: string | null) => { authToken = token; };
|
||||
|
||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
||||
export const apiFetch = async <T>(path: string, options: RequestInit = {}): Promise<T> => {
|
||||
const headers = { "Content-Type": "application/json", ...options.headers };
|
||||
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
||||
const res = await fetch(`${API_URL}${path}`, { ...options, headers });
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
return body as T;
|
||||
};
|
||||
```
|
||||
|
||||
El token se setea automáticamente tras `login` o `register` (dentro de `auth.service.ts`).
|
||||
|
||||
### Services (capa fina sobre apiFetch)
|
||||
|
||||
- **auth.service.ts** — register, login, getMe
|
||||
- **tracking.service.ts** — getMyStatus, sendGpsUpdate, resetDemo
|
||||
- **addresses.service.ts** — listColonias, getMyAddress, setMyAddress
|
||||
- **feedback.service.ts** — submitFeedback, listMyFeedback
|
||||
- **admin.service.ts** — listAllRoutes, cancelRoute, resumeRoute
|
||||
|
||||
---
|
||||
|
||||
## Componentes reutilizables
|
||||
|
||||
| Componente | Para qué |
|
||||
|---|---|
|
||||
| `EtaCard` | Card grande del ETA con "X min" gigante + ventana |
|
||||
| `AlertItem` (`Alertltem.tsx`) | Item de notificación con icon + título + body + tiempo + badge por tipo |
|
||||
| `InputField` | Wrap de TextInput con estilos del proyecto |
|
||||
| `PrimaryButton` | Botón principal verde |
|
||||
| `QuickAction` | Card cuadrada con icon circular + título (usada en home) |
|
||||
| `SectionTitle` | Título grande de sección |
|
||||
| `StatusBadge` | (no usado actualmente) |
|
||||
| `CollectionCalendar` | Calendario mensual con colores por día según ruta del user |
|
||||
| `NotificationToast` | Banner verde animado que cae cuando llega una notif nueva |
|
||||
|
||||
---
|
||||
|
||||
## Roles y navegación
|
||||
|
||||
### USER
|
||||
- Ve las tabs: Inicio, Alertas, Guía, Perfil
|
||||
- Polling activo
|
||||
- Al loguearse, lo redirige a `/` (Home)
|
||||
- Si entra a `/` sin login → redirect a `/login`
|
||||
- Si entra a una pantalla protegida sin domicilio validado → algunas muestran CTA "Validar domicilio"
|
||||
|
||||
### ADMIN
|
||||
- Ve solo la tab: **Admin**
|
||||
- Las demás tabs del user están ocultas (`href: null`)
|
||||
- NO hace polling de `/status`
|
||||
- Al loguearse desde `/login` aterriza en `/` y desde ahí redirect a `/admin`
|
||||
- Sus acciones (cancelar / reanudar) refrescan el listado cada 15 s
|
||||
|
||||
---
|
||||
|
||||
## Notificaciones
|
||||
|
||||
### En foreground (Toast in-app)
|
||||
- Cuando llega una notificación nueva del polling, se setea `toast` en el AppContext
|
||||
- El componente `NotificationToast` anima un banner verde que cae desde arriba
|
||||
- Se autodescarta a los 5 segundos
|
||||
- Funciona en cualquier pantalla porque vive en `_layout.tsx`
|
||||
|
||||
### Push del SO con app cerrada
|
||||
- **No funciona en Expo Go SDK 53+** — Expo removió el soporte
|
||||
- El módulo `expo-notifications` está instalado y el helper `lib/notifications.ts` está listo
|
||||
- Para activarlo en producción → crear Development Build con EAS
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Síntoma | Causa | Fix |
|
||||
|---|---|---|
|
||||
| `Network request failed` | `API_URL` apunta a dirección inalcanzable desde el dispositivo | Usa `10.0.2.2:8080` (emulador) o IP LAN (físico) |
|
||||
| `Failed to fetch` en navegador web | CORS | Activar `cors()` middleware en el backend |
|
||||
| 401 al cargar `/status` | Token vencido o no enviado | Logout + login otra vez |
|
||||
| "Cannot find module ../config/api.js" | Importaste con `.js` (estilo backend) | En frontend, importa sin extensión: `from "../config/api"` |
|
||||
| `Unable to resolve module` | Metro caché stale | `npx expo start -c` |
|
||||
| App muestra "12 min / En Camino" hardcoded | Te perdiste de actualizar `EtaCard` con props del backend | Verificar que home pasa `minutes` y `status` |
|
||||
| Versión de SDK incompatible | Expo Go diferente al del proyecto (56) | Actualizar Expo Go en App Store / Play Store |
|
||||
| Tab Admin no aparece | Login devolvió role ≠ "ADMIN" | Verifica logueado con `admin@test.com / admin123` |
|
||||
|
||||
---
|
||||
|
||||
## Para extender
|
||||
|
||||
- **Nueva pantalla**: crea archivo en `src/app/X.tsx`. expo-router la detecta. Si no quieres que aparezca en el tab bar, agrégala con `options={{ href: null }}` en `_layout.tsx`.
|
||||
- **Nuevo service**: crea `services/X.service.ts` que use `apiFetch`. Importa donde lo necesites.
|
||||
- **Nuevo dato global**: agrega al `AppContext` y expón en el value.
|
||||
- **Imágenes nuevas**: ponlas en `assets/illustrations/` y úsalas con `require()`.
|
||||
|
||||
---
|
||||
|
||||
## Frase de arquitectura para la exposición
|
||||
|
||||
> "El frontend hace polling controlado cada 30 s al endpoint `/api/tracking/status`. Esto evita sobrecargar el servidor y es predecible. El AppContext detecta notificaciones nuevas con un Set de IDs ya vistos y dispara un Toast in-app. Para push del SO con app cerrada migraríamos a Development Build y expo-notifications real, pero la decisión de Expo Go nos limita en esta etapa."
|
||||
|
||||
@@ -24,9 +24,12 @@ import { COLORS } from "../constants/colors";
|
||||
import { useApp } from "../context/AppContext";
|
||||
import {
|
||||
cancelRoute,
|
||||
listAllFeedback,
|
||||
listAllRoutes,
|
||||
resumeRoute,
|
||||
type AdminFeedbackItem,
|
||||
type AdminRouteItem,
|
||||
type FeedbackType,
|
||||
} from "../services/admin.service";
|
||||
|
||||
const arrivalLabel: Record<string, { label: string; color: string }> = {
|
||||
@@ -39,28 +42,36 @@ const arrivalLabel: Record<string, { label: string; color: string }> = {
|
||||
export default function AdminScreen() {
|
||||
const { user, logout } = useApp();
|
||||
const [routes, setRoutes] = useState<AdminRouteItem[]>([]);
|
||||
const [feedback, setFeedback] = useState<AdminFeedbackItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchRoutes = useCallback(async () => {
|
||||
const fetchAll = useCallback(async () => {
|
||||
try {
|
||||
const data = await listAllRoutes();
|
||||
setRoutes(data);
|
||||
const [routesData, feedbackData] = await Promise.all([
|
||||
listAllRoutes(),
|
||||
listAllFeedback(),
|
||||
]);
|
||||
setRoutes(routesData);
|
||||
setFeedback(feedbackData);
|
||||
} catch (err) {
|
||||
Alert.alert(
|
||||
"Error",
|
||||
err instanceof Error ? err.message : "No se pudieron cargar las rutas",
|
||||
err instanceof Error ? err.message : "No se pudieron cargar los datos",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Alias para compatibilidad con el resto del archivo
|
||||
const fetchRoutes = fetchAll;
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.role !== "ADMIN") return;
|
||||
void fetchRoutes();
|
||||
const interval = setInterval(() => void fetchRoutes(), 15_000);
|
||||
void fetchAll();
|
||||
const interval = setInterval(() => void fetchAll(), 15_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [user, fetchRoutes]);
|
||||
}, [user, fetchAll]);
|
||||
|
||||
if (!user) return <Redirect href="/login" />;
|
||||
if (user.role !== "ADMIN") return <Redirect href="/" />;
|
||||
@@ -259,12 +270,127 @@ export default function AdminScreen() {
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* === Reportes de ciudadanos === */}
|
||||
<Text style={[styles.sectionTitle, { marginTop: 14 }]}>
|
||||
REPORTES DE CIUDADANOS ({feedback.length})
|
||||
</Text>
|
||||
|
||||
{feedback.length === 0 ? (
|
||||
<View style={styles.emptyFeedback}>
|
||||
<Ionicons name="chatbubbles-outline" size={28} color="#9CA3AF" />
|
||||
<Text style={styles.emptyFeedbackText}>
|
||||
Aún no hay reportes ciudadanos. Aparecerán aquí cuando los
|
||||
usuarios usen el buzón.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
feedback.map((f) => {
|
||||
const meta = feedbackMeta[f.type] ?? feedbackMeta.OTHER;
|
||||
const when = new Date(f.createdAt).toLocaleString("es-MX", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
return (
|
||||
<View key={f.id} style={styles.feedbackCard}>
|
||||
<View style={styles.feedbackHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.feedbackIcon,
|
||||
{ backgroundColor: `${meta.color}20` },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={meta.icon}
|
||||
size={18}
|
||||
color={meta.color}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.feedbackType}>{meta.label}</Text>
|
||||
<Text style={styles.feedbackMeta}>
|
||||
{f.userName ?? `Usuario #${f.userId}`}
|
||||
{f.colonia ? ` · ${f.colonia}` : ""} · {when}
|
||||
</Text>
|
||||
</View>
|
||||
{f.routeId ? (
|
||||
<View style={styles.routeChip}>
|
||||
<Ionicons
|
||||
name="bus-outline"
|
||||
size={11}
|
||||
color="#0E8A61"
|
||||
/>
|
||||
<Text style={styles.routeChipText}>{f.routeId}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.routeChip,
|
||||
{ backgroundColor: "#F3F4F6" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.routeChipText, { color: "#9CA3AF" }]}
|
||||
>
|
||||
Sin ruta
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{f.rating ? (
|
||||
<View style={styles.starsRow}>
|
||||
{[1, 2, 3, 4, 5].map((n) => (
|
||||
<Ionicons
|
||||
key={n}
|
||||
name={n <= (f.rating ?? 0) ? "star" : "star-outline"}
|
||||
size={14}
|
||||
color="#F59E0B"
|
||||
style={{ marginRight: 2 }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Text style={styles.feedbackMessage}>{f.message}</Text>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const feedbackMeta: Record<
|
||||
FeedbackType,
|
||||
{ label: string; icon: keyof typeof Ionicons.glyphMap; color: string }
|
||||
> = {
|
||||
TRUCK_DID_NOT_PASS: {
|
||||
label: "El camión no pasó",
|
||||
icon: "alert-circle-outline",
|
||||
color: "#EF4444",
|
||||
},
|
||||
RATING: {
|
||||
label: "Calificación del servicio",
|
||||
icon: "star-outline",
|
||||
color: "#F59E0B",
|
||||
},
|
||||
SUGGESTION: {
|
||||
label: "Sugerencia",
|
||||
icon: "bulb-outline",
|
||||
color: "#3B82F6",
|
||||
},
|
||||
OTHER: {
|
||||
label: "Otro",
|
||||
icon: "chatbubble-outline",
|
||||
color: "#6B7280",
|
||||
},
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
@@ -372,4 +498,75 @@ const styles = StyleSheet.create({
|
||||
marginTop: 2,
|
||||
textAlign: "center",
|
||||
},
|
||||
emptyFeedback: {
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
emptyFeedbackText: {
|
||||
fontSize: 12,
|
||||
color: "#6B7280",
|
||||
textAlign: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
feedbackCard: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
marginBottom: 10,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 4,
|
||||
elevation: 1,
|
||||
},
|
||||
feedbackHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
},
|
||||
feedbackIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10,
|
||||
},
|
||||
feedbackType: {
|
||||
fontSize: 13,
|
||||
fontWeight: "800",
|
||||
color: "#0F172A",
|
||||
},
|
||||
feedbackMeta: {
|
||||
fontSize: 10,
|
||||
color: "#6B7280",
|
||||
marginTop: 1,
|
||||
},
|
||||
routeChip: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#ECFDF5",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 999,
|
||||
marginLeft: 6,
|
||||
},
|
||||
routeChipText: {
|
||||
fontSize: 11,
|
||||
fontWeight: "800",
|
||||
color: "#0E8A61",
|
||||
marginLeft: 4,
|
||||
},
|
||||
starsRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 6,
|
||||
},
|
||||
feedbackMessage: {
|
||||
fontSize: 13,
|
||||
color: "#374151",
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,9 +12,30 @@ export interface AdminRouteItem {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export type FeedbackType =
|
||||
| "TRUCK_DID_NOT_PASS"
|
||||
| "RATING"
|
||||
| "SUGGESTION"
|
||||
| "OTHER";
|
||||
|
||||
export interface AdminFeedbackItem {
|
||||
id: string;
|
||||
userId: number;
|
||||
routeId: string | null;
|
||||
userName?: string;
|
||||
colonia?: string;
|
||||
type: FeedbackType;
|
||||
message: string;
|
||||
rating?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const listAllRoutes = () =>
|
||||
apiFetch<AdminRouteItem[]>("/api/admin/routes");
|
||||
|
||||
export const listAllFeedback = () =>
|
||||
apiFetch<AdminFeedbackItem[]>("/api/admin/feedback");
|
||||
|
||||
export const cancelRoute = (routeId: string, reason?: string) =>
|
||||
apiFetch<{ message: string; routeId: string }>(
|
||||
`/api/admin/routes/${routeId}/cancel`,
|
||||
|
||||
Reference in New Issue
Block a user