πŸ› οΈ TRD - Rutas de Solidaridad

Technical Requirements Document - Blueprint TΓ©cnico

⚑ Aprobado para Desarrollo

1VisiΓ³n TΓ©cnica General

Objetivo: Construir una PWA robusta, escalable y offline-first para gestiΓ³n de ayuda humanitaria en emergencias.

⚑ Principios: Offline First · Real Time · Mobile First · Seguridad RLS · Escalabilidad · Código Limpio
πŸ“¦
Offline First
Funcionalidad sin conexiΓ³n
⚑
Real Time
Actualizaciones en vivo
πŸ“±
Mobile First
Prioridad mΓ³vil
πŸ”’
Seguridad
RLS + JWT + Zod

Restricciones TΓ©cnicas

2Stack TecnolΓ³gico

Frontend

βš›οΈ
React
18.3.1
Framework principal
⚑
Vite
5.0.0
Build tool rΓ‘pido
🎨
Tailwind CSS
3.4.0
Estilos rΓ‘pidos
🧭
React Router
6.23.0
NavegaciΓ³n SPA
πŸ—ΊοΈ
Leaflet
1.9.4
Mapas gratuitos
πŸ“
React Hook Form
7.51.0
Formularios eficientes
βœ…
Zod
3.23.0
ValidaciΓ³n TypeScript
πŸ“¦
React Query
5.40.0
CachΓ© y estado

Backend & Servicios

πŸ—„οΈ
Supabase
Postgres + Auth + Storage + Realtime
🐘
PostgreSQL
Base de datos relacional
πŸ”
Supabase Auth
JWT + Email/Password
🌍
OpenStreetMap
Mapas gratuitos sin lΓ­mites

3Arquitectura de la AplicaciΓ³n

Diagrama de Arquitectura

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLIENTE (PWA) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ React SPA - Vite Build β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚Dashboardβ”‚Inventarβ”‚ Mapa β”‚Report β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚Afectadoβ”‚Donacionβ”‚Voluntarβ”‚ Ayuda β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ React Query (CachΓ© y Estado) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Supabase Client (API) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SUPABASE (Backend) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ PostgreSQL + RLS + Auth + Storage β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Estructura de Carpetas

src/ β”œβ”€β”€ components/ β”‚ β”œβ”€β”€ ui/ # Botones, inputs, cards β”‚ β”œβ”€β”€ layout/ # Navbar, Sidebar, Footer β”‚ β”œβ”€β”€ dashboard/ β”‚ β”œβ”€β”€ inventario/ β”‚ β”œβ”€β”€ afectados/ β”‚ β”œβ”€β”€ donaciones/ β”‚ β”œβ”€β”€ voluntarios/ β”‚ β”œβ”€β”€ asignaciones/ β”‚ └── mapas/ β”œβ”€β”€ lib/ β”‚ β”œβ”€β”€ supabase.js # Cliente Supabase β”‚ β”œβ”€β”€ queries.js # Consultas β”‚ β”œβ”€β”€ mutations.js # Mutaciones β”‚ └── helpers.js β”œβ”€β”€ hooks/ β”‚ β”œβ”€β”€ useAuth.js β”‚ β”œβ”€β”€ useInventario.js β”‚ └── useGeolocation.js β”œβ”€β”€ pages/ β”‚ β”œβ”€β”€ Dashboard.jsx β”‚ β”œβ”€β”€ Afectados.jsx β”‚ β”œβ”€β”€ Inventario.jsx β”‚ β”œβ”€β”€ Donaciones.jsx β”‚ β”œβ”€β”€ Voluntarios.jsx β”‚ β”œβ”€β”€ Asignaciones.jsx β”‚ β”œβ”€β”€ Mapa.jsx β”‚ β”œβ”€β”€ Perfil.jsx β”‚ └── Login.jsx β”œβ”€β”€ context/ β”‚ └── AuthContext.jsx β”œβ”€β”€ utils/ β”‚ β”œβ”€β”€ validations.js # Zod schemas β”‚ β”œβ”€β”€ formats.js β”‚ └── constants.js β”œβ”€β”€ styles/ β”‚ └── index.css └── App.jsx

4Frontend - Especificaciones TΓ©cnicas

Vite Configuration

import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { outDir: 'dist', sourcemap: true, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router-dom'], supabase: ['@supabase/supabase-js'], maps: ['leaflet', 'react-leaflet'] } } } }, server: { port: 5173, host: true } })

Cliente Supabase

import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: true, autoRefreshToken: true }, realtime: { params: { eventsPerSecond: 10 } } }) export const getCurrentUser = async () => { const { data: { user } } = await supabase.auth.getUser() return user }

Tailwind Config

export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: { colors: { primary: { DEFAULT: '#EF4444', light: '#FCA5A5', dark: '#B91C1C' }, success: '#10B981', warning: '#F59E0B', danger: '#EF4444' }, fontFamily: { sans: ['Inter', 'sans-serif'] } } }, plugins: [] }

5Backend - Especificaciones TΓ©cnicas

ConfiguraciΓ³n de Supabase

URL
https://nvxgygnrgjnddrtboskr.supabase.co
RegiΓ³n
Americas (us-east-1)
Auth
Email/Password + JWT
SesiΓ³n
7 dΓ­as con refresh token

Roles de Usuario

RolAcceso
adminAcceso total al sistema
voluntarioAcceso a datos operativos
publicSolo registro de afectados

PolΓ­ticas RLS

-- Voluntarios pueden ver afectados CREATE POLICY "Voluntarios ver afectados" ON afectados FOR SELECT USING (auth.role() IN ('admin', 'voluntario')); -- Cualquiera puede registrar afectados CREATE POLICY "Publico registrar afectados" ON afectados FOR INSERT WITH CHECK (true); -- Solo admins pueden modificar voluntarios CREATE POLICY "Admins gestionar voluntarios" ON voluntarios FOR ALL USING (auth.role() = 'admin');

6Base de Datos - DiseΓ±o Detallado

Tablas del Sistema

#TablaPropΓ³sito
1ubicacionesCentros de acopio
2categorias_productosCategorΓ­as de productos
3productosCatΓ‘logo de productos
4voluntariosDatos de voluntarios
5afectadosDatos de afectados
6donacionesRegistro de donaciones
7inventario_ubicacionStock por ubicaciΓ³n
8movimientos_inventarioHistorial de movimientos
9asignacionesAsignaciΓ³n de ayudas
10alertas_inventarioAlertas automΓ‘ticas
11seguimiento_afectadosHistorial de seguimiento

Triggers AutomΓ‘ticos

-- Actualizar inventario automΓ‘ticamente CREATE OR REPLACE FUNCTION actualizar_inventario() RETURNS TRIGGER AS $$ BEGIN IF NEW.tipo_movimiento = 'ingreso' THEN INSERT INTO inventario_ubicacion (producto_id, ubicacion_id, cantidad) VALUES (NEW.producto_id, NEW.ubicacion_destino_id, NEW.cantidad) ON CONFLICT (producto_id, ubicacion_id) DO UPDATE SET cantidad = inventario_ubicacion.cantidad + EXCLUDED.cantidad; ELSIF NEW.tipo_movimiento = 'salida' THEN UPDATE inventario_ubicacion SET cantidad = cantidad - NEW.cantidad WHERE producto_id = NEW.producto_id AND ubicacion_id = NEW.ubicacion_origen_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql;

Datos Iniciales (Seed)

-- CategorΓ­as de productos INSERT INTO categorias_productos (nombre, descripcion, unidad_medida, icono) VALUES ('Alimentos', 'Alimentos no perecederos', 'kg', 'πŸ₯«'), ('Agua', 'Agua potable', 'litros', 'πŸ’§'), ('Medicinas', 'Medicamentos', 'unidades', 'πŸ’Š'), ('Ropa', 'Ropa y calzado', 'prendas', 'πŸ‘•'), ('Higiene', 'Higiene personal', 'unidades', '🧴'), ('BebΓ©s', 'Productos infantiles', 'unidades', '🍼'); -- Centros de acopio INSERT INTO ubicaciones (nombre, direccion, tipo) VALUES ('Catia La Mar', 'Catia La Mar, La Guaira', 'centro_acopio'), ('Caraballeda', 'Caraballeda, La Guaira', 'centro_acopio'), ('MaiquetΓ­a', 'MaiquetΓ­a, La Guaira', 'centro_acopio');

7API - DiseΓ±o y Endpoints

Endpoints Principales

MΓ©todoEndpointPropΓ³sito
GET/afectadosListar afectados
POST/afectadosRegistrar afectado
PATCH/afectados?id=eq.{id}Actualizar afectado
GET/inventario_ubicacionVer inventario
POST/movimientos_inventarioRegistrar movimiento
GET/voluntariosListar voluntarios
POST/donacionesRegistrar donaciΓ³n
GET/asignacionesVer asignaciones
POST/asignacionesCrear asignaciΓ³n

Ejemplo: Registrar Afectado

const registrarAfectado = async (data) => { const { data: result, error } = await supabase .from('afectados') .insert([{ cedula: data.cedula, nombre: data.nombre, latitud: data.latitud, longitud: data.longitud, adultos: data.adultos, ninos: data.ninos, necesidades: data.necesidades }]) .select() return { result, error } }

Tiempo Real (WebSockets)

const subscribeInventario = (callback) => { return supabase .channel('inventario') .on('postgres_changes', { event: '*', schema: 'public', table: 'inventario_ubicacion' }, callback ) .subscribe() }

8PWA - ConfiguraciΓ³n y Estrategia

Manifest

{ "name": "Rutas de Solidaridad", "short_name": "RutasSolidaridad", "display": "standalone", "background_color": "#FFFFFF", "theme_color": "#EF4444", "orientation": "portrait", "icons": [ { "src": "/icon-192.png", "sizes": "192x192" }, { "src": "/icon-512.png", "sizes": "512x512" } ] }

Estrategia Offline

Cache First
HTML, JS, CSS, imΓ‘genes
Stale While Revalidate
Datos de API
Network First
Operaciones dinΓ‘micas
Queue Offline
SincronizaciΓ³n al reconectar
const pendingOperations = [] const syncOffline = async () => { if (navigator.onLine) { for (const op of pendingOperations) { await supabase.from(op.table).insert(op.data) } pendingOperations = [] localStorage.setItem('pendingOps', JSON.stringify(pendingOperations)) } }

9Seguridad - ImplementaciΓ³n

πŸ”
AutenticaciΓ³n
Supabase Auth + JWT
πŸ›‘οΈ
AutorizaciΓ³n
RLS Row Level Security
βœ…
ValidaciΓ³n
Zod en frontend y backend
πŸ”’
HTTPS
Forzado en producciΓ³n

ValidaciΓ³n con Zod

import { z } from 'zod' const afectadoSchema = z.object({ cedula: z.string().min(6, 'CΓ©dula invΓ‘lida'), nombre: z.string().min(2, 'Nombre requerido'), telefono: z.string().optional(), adultos: z.number().min(0).default(0), ninos: z.number().min(0).default(0), necesidades: z.array(z.string()).default([]) }) const validarAfectado = (data) => { try { return afectadoSchema.parse(data) } catch (error) { return { error: error.errors } } }

10Despliegue - Estrategia

Entornos

EntornoURLPropΓ³sito
Devlocalhost:5173Desarrollo local
Stagingvoluntariado.foresve.com/stagingPruebas
Prodvoluntariado.foresve.comProducciΓ³n

Proceso de Despliegue

# 1. Build de producciΓ³n npm run build # 2. Verificar localmente npm run preview # 3. Subir carpeta dist/ por FTP # Usar FTP-Simple # 4. Verificar despliegue # Abrir https://voluntariado.foresve.com

.htaccess para SPA + CachΓ©

RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] "\.(js|css|png|jpg|jpeg|gif|svg|ico)$"> Header set Cache-Control "max-age=31536000, public"

11Rendimiento - Optimizaciones

ÁreaTécnicaImpacto
CargaLazy loading de rutas-40% carga inicial
ImΓ‘genesOptimizaciΓ³n con Vite-50% tamaΓ±o
CΓ³digoCode splitting automΓ‘tico-30% bundle
CachΓ©Service WorkerCarga instantΓ‘nea
DataReact Query con cachΓ©-70% llamadas API
MapasCarga bajo demanda-60% tiempo

MΓ©tricas Objetivo

FCP
< 1.5s
LCP
< 2.5s
TTI
< 3s
Lighthouse
> 90

12Testing - Estrategia

Unit Testing
Componentes y funciones
Integration
Conexiones con Supabase
E2E
Flujos completos (opcional)
import { render, screen } from '@testing-library/react' import { RegistroAfectado } from './RegistroAfectado' test('renderiza formulario de afectado', () => { render() expect(screen.getByText('Registrar Afectado')).toBeInTheDocument() })

13Monitoreo - Herramientas

Supabase Dashboard
BD y rendimiento
React DevTools
Debugging frontend
Sentry
Errores en producciΓ³n
const logActivity = (action, data) => { console.log(`[${new Date().toISOString()}] ${action}:`, data) }

15Roadmap TΓ©cnico

πŸ”΄ Semana 1 - FundaciΓ³n

DΓ­as 1-3

  • Configurar proyecto Vite
  • Conectar con Supabase
  • Estructura de carpetas
  • AutenticaciΓ³n bΓ‘sica
  • Layout base

🟑 Semana 1 - Funcionalidad

DΓ­as 4-7

  • Registro de afectados
  • Dashboard bΓ‘sico
  • Inventario simple
  • CRUD de productos
  • Movimientos inventario

🟒 Semana 2 - Operaciones

DΓ­as 8-14

  • GestiΓ³n de donaciones
  • AsignaciΓ³n de ayudas
  • Mapa de necesidades
  • Seguimiento entregas
  • Perfil voluntarios

πŸ”΅ Semana 3 - PWA

DΓ­as 15-21

  • ConfiguraciΓ³n PWA
  • Modo offline
  • SincronizaciΓ³n
  • Notificaciones push
  • Reportes exportables

βšͺ Semana 4 - ProducciΓ³n

DΓ­as 22-28

  • Testing completo
  • OptimizaciΓ³n rendimiento
  • DocumentaciΓ³n final
  • Despliegue a producciΓ³n
  • CapacitaciΓ³n

πŸ“‹ Checklist TΓ©cnico Final

  • Repositorio con estructura definida
  • ConexiΓ³n a Supabase funcionando
  • Variables de entorno configuradas
  • AutenticaciΓ³n implementada
  • RLS configurado y probado
  • PWA configurada
  • Modo offline funcionando
  • Mapa integrado
  • Inventario en tiempo real
  • Registro de afectados funcionando
  • Sistema de donaciones completo
  • Asignaciones operativas
  • Reportes exportables
  • DocumentaciΓ³n completa
  • Despliegue en producciΓ³n