Version CGL

This commit is contained in:
2026-02-22 13:35:16 +01:00
commit 8c4d0d4d06
8 changed files with 345 additions and 0 deletions

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
# En Docker, los comandos se ejecutan como root por defecto, no hace falta sudo
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

90
control.sh Normal file
View File

@@ -0,0 +1,90 @@
#!/bin/bash
# Colores para los mensajes
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Función de ayuda
show_help() {
echo -e "${BLUE}===================== CONTROL DEL BLOG DE JOSEMI =================${NC}"
echo " "
echo " Uso: ./control.sh [comando] "
echo ""
echo "Comandos disponibles:"
echo -e " ${GREEN}start${NC} -> Levanta todo el entorno (en segundo plano)"
echo -e " ${GREEN}stop${NC} -> Detiene los contenedores (sin borrarlos)"
echo -e " ${GREEN}restart${NC} -> Reinicia los contenedores"
echo -e " ${GREEN}build${NC} -> Reconstruye las imágenes (útil si instalas nuevas librerías)"
echo -e " ${GREEN}logs${NC} -> Ver logs de todo en tiempo real (Ctrl+C para salir)"
echo -e " ${GREEN}status${NC} -> Ver estado de los contenedores"
echo -e " ${GREEN}shell-be${NC} -> Entrar a la terminal del Backend"
echo -e " ${GREEN}shell-fe${NC} -> Entrar a la terminal del Frontend"
echo -e " ${RED}reset${NC} -> ¡PELIGRO! Borra la Base de Datos y la crea de cero (útil si cambias contraseñas)"
echo -e " ${YELLOW}info${NC} -> Muestra las URLs y credenciales de acceso"
echo -e " ${RED}down${NC} -> Deletea todo el container"
echo "=================================================================="
}
# Lógica del script
case "$1" in
start)
echo -e "${GREEN}Arrancando el sistema...${NC}"
sudo docker-compose up -d
echo -e "${BLUE}¡Listo! Usa './control.sh info' para ver los accesos.${NC}"
;;
stop)
echo -e "${YELLOW}Deteniendo contenedores...${NC}"
sudo docker-compose stop
;;
restart)
echo -e "${YELLOW}Reiniciando...${NC}"
sudo docker-compose restart
;;
build)
echo -e "${BLUE}Reconstruyendo imágenes (esto puede tardar)...${NC}"
sudo docker-compose up -d --build
;;
logs)
echo -e "${BLUE}Mostrando logs (Ctrl+C para salir)...${NC}"
sudo docker-compose logs -f
;;
status)
sudo docker-compose ps
;;
shell-be)
echo -e "${GREEN}Entrando al contenedor del Backend...${NC}"
sudo docker exec -it blog_backend sh
;;
shell-fe)
echo -e "${GREEN}Entrando al contenedor del Frontend...${NC}"
sudo docker exec -it blog_frontend sh
;;
reset)
echo -e "${RED}ATENCIÓN: Esto borrará todos los datos de la base de datos.${NC}"
read -p "¿Estás seguro? (s/n): " confirm
if [[ $confirm == [sS] || $confirm == [sS][yY] ]]; then
echo -e "${YELLOW}Borrando todo...${NC}"
sudo docker-compose down -v
sudo docker system prune -a
echo -e "${GREEN}Levantando de nuevo...${NC}"
sudo docker-compose up -d
else
echo "Operación cancelada."
fi
;;
info)
echo -e "${BLUE}=== INFORMACIÓN DE ACCESO ===${NC}"
echo -e "WEB: ${GREEN}http://localhost:3000${NC}"
echo -e "${BLUE}======================${NC}"
;;
down)
sudo docker-compose down -v
sudo docker system prune -a
;;
*)
show_help
;;
esac

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
frontend:
build: .
container_name: creapeticiones_frontend
ports:
- "3000:80"
restart: always

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Creapeticiones | Generador</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "creapeticiones",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.3.0"
}
}

184
src/App.jsx Normal file
View File

@@ -0,0 +1,184 @@
import React, { useState, useEffect } from 'react';
export default function App() {
const [pagina, setPagina] = useState('menu');
const [resultado, setResultado] = useState('-----');
const [filtros, setFiltros] = useState({
origen: '', destino: '', operador: 'Renfe Mercancías', ultHora: false, inversion: false
});
// --- BASE DE DATOS ESTRUCTURADA ---
const estacionesSur = [
"Estación Sur", "Ingeniero Pizias", "Carlos Abadias", "Bifuracion Anillo", "Princesa Susana",
"Aguja - Princesa Susana", "Santa Ana", "CAF", "Gadea", "Armengol", "Domingo Gil",
"Pueblo Novo", "Aguja-Central Combustible", "Casacota", "Pinares", "Suso", "Los Zorros",
"Carles Romero", "Dos hermanos", "Genzor", "Aranda", "Bifurcacion Aranda", "Huerta", "Calera", "Orobon"
];
const estacionesNorte = [
"Carthago", "Llamas", "Castañeda", "Arenillas", "Pujazon", "Sanchez - Avila",
"Poveda", "Estacion Norte", "Rovira", "Aguja Salvador Hernandez", "Salvador y Hernandez - Plaza"
];
// Eje para calcular paridad (Orden de NORTE a SUR = PAR)
const ejePar = [
"Salvador y Hernandez - Plaza", "Aguja Salvador Hernandez", "Rovira", "Estacion Norte",
"Poveda", "Sanchez - Avila", "Pujazon", "Arenillas", "Castañeda", "Llamas", "Carthago",
"Calera", "Huerta", "Bifurcacion Aranda", "Genzor", "Dos hermanos", "Aranda", "Carles Romero",
"Los Zorros", "Orobon", "Princesa Susana", "Aguja - Princesa Susana", "Santa Ana", "CAF",
"Ingeniero Pizias", "Carlos Abadias", "Estación Sur"
];
const todasLasEstaciones = [...new Set([...estacionesSur, ...estacionesNorte])].sort();
// --- SISTEMA DE MEMORIA ---
const getUltimo = (tipo) => parseInt(localStorage.getItem(`ultimo_${tipo}`)) || 1000;
const saveUltimo = (tipo, val) => localStorage.setItem(`ultimo_${tipo}`, val);
const generarPeticion = (esEspecial) => {
const { origen, destino, operador, ultHora, inversion } = filtros;
if (!origen || !destino) return alert("Error: Debe indicar origen y destino.");
// 1. DETERMINAR SENTIDO Y PARIDAD
const idxO = ejePar.indexOf(origen);
const idxD = ejePar.indexOf(destino);
// Si baja (de Norte a Sur) es PAR. Si sube es IMPAR.
// Ramales específicos: Santa Ana-Armengol y ramal Casacota se consideran subida (IMPAR)
let paridadRequerida = "IMPAR";
if (idxO < idxD && idxO !== -1 && idxD !== -1) paridadRequerida = "PAR";
// 2. CONSTRUIR PREFIJO
let prefijo = "";
if (esEspecial) {
let segundo = "0";
const esOrigenSur = estacionesSur.includes(origen);
const esDestinoSur = estacionesSur.includes(destino);
const esOrigenNorte = estacionesNorte.includes(origen);
const esDestinoNorte = estacionesNorte.includes(destino);
const esLAV = (origen === "Genzor" || origen === "Castañeda") && (destino === "Genzor" || destino === "Castañeda");
if (ultHora) segundo = "0";
else if (esLAV) segundo = "7";
else if (esOrigenSur && esDestinoSur) segundo = "8";
else if (esOrigenNorte && esDestinoNorte) segundo = "9";
prefijo = `9${segundo}`;
} else {
// Regulares Mercancías
const p1 = operador === "Renfe Mercancías" ? "5" : (operador === "Privada" ? "6" : "8");
const p2 = estacionesNorte.includes(origen) ? "9" : "8";
const p3 = estacionesNorte.includes(destino) ? "9" : "8";
prefijo = `${p1}${p2}${p3}`;
}
// 3. CÁLCULO DEL NÚMERO (+1 y Paridad)
const tipoMem = esEspecial ? 'esp' : 'reg';
let base = getUltimo(tipoMem) + 1;
const ajustarParidad = (num, paridad) => {
if (paridad === "PAR") return num % 2 === 0 ? num : num + 1;
return num % 2 !== 0 ? num : num + 1;
};
let numeroFinal = ajustarParidad(base, paridadRequerida);
saveUltimo(tipoMem, numeroFinal);
// 4. FORMATO FINAL
let strFinal = `${prefijo}${String(numeroFinal).slice(-2)}`;
if (esEspecial) strFinal = `${prefijo}${String(numeroFinal).slice(-3)}`;
if (inversion) {
const paridadInversa = paridadRequerida === "PAR" ? "IMPAR" : "PAR";
setResultado(`${strFinal} / ${paridadInversa}`);
} else {
setResultado(strFinal);
}
};
const resetMemoria = () => {
if(confirm("¿Resetear todos los contadores?")) {
localStorage.clear();
setResultado("-----");
}
};
const estilos = {
main: { display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', backgroundColor: '#050505', color: '#fff', fontFamily: 'monospace' },
card: { backgroundColor: '#111', padding: '30px', borderRadius: '4px', width: '100%', maxWidth: '550px', border: '1px solid #333', boxShadow: '0 0 20px #000' },
nav: { display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #222', paddingBottom: '10px', marginBottom: '20px' },
btnMenu: (col) => ({ width: '100%', padding: '15px', margin: '5px 0', border: `1px solid \${col}`, color: col, backgroundColor: 'transparent', cursor: 'pointer', fontWeight: 'bold' }),
select: { width: '100%', padding: '12px', margin: '8px 0', backgroundColor: '#1a1a1a', color: '#00d1b2', border: '1px solid #333', outline: 'none' },
display: { fontSize: '3.5rem', textAlign: 'center', padding: '20px', color: '#00d1b2', border: '2px solid #00d1b2', margin: '20px 0', backgroundColor: '#000', letterSpacing: '4px' },
footer: { display: 'flex', justifyContent: 'space-between', marginTop: '20px', fontSize: '0.7rem', color: '#444' }
};
return (
<div style={estilos.main}>
<div style={estilos.card}>
{pagina === 'menu' ? (
<div style={{textAlign: 'center'}}>
<h1 style={{color: '#00d1b2', letterSpacing: '5px'}}>AGER v0.2</h1>
<p style={{color: '#444', marginBottom: '30px'}}>SISTEMA DE PETICIÓN DE SURCOS</p>
<button style={estilos.btnMenu('#00d1b2')} onClick={() => setPagina('especiales')}>[ ESPECIALES 9XXXX ]</button>
<button style={estilos.btnMenu('#3498db')} onClick={() => setPagina('regulares')}>[ REGULARES MERC. ]</button>
<button style={{...estilos.btnMenu('#555'), marginTop: '40px', fontSize: '0.7rem'}} onClick={resetMemoria}>RESET TOTAL MEMORIA</button>
</div>
) : (
<div>
<div style={estilos.nav}>
<span style={{color: pagina === 'especiales' ? '#00d1b2' : '#3498db'}}>MODO_{pagina.toUpperCase()}</span>
<span style={{cursor: 'pointer'}} onClick={() => {setPagina('menu'); setResultado('-----');}}>VOLVER_AL_MENU</span>
</div>
<label style={{fontSize: '0.7rem'}}>PUNTO DE ORIGEN</label>
<select style={estilos.select} value={filtros.origen} onChange={(e) => setFiltros({...filtros, origen: e.target.value})}>
<option value="">-- SELECCIONAR --</option>
{todasLasEstaciones.map(e => <option key={e} value={e}>{e}</option>)}
</select>
<label style={{fontSize: '0.7rem'}}>PUNTO DE DESTINO</label>
<select style={estilos.select} value={filtros.destino} onChange={(e) => setFiltros({...filtros, destino: e.target.value})}>
<option value="">-- SELECCIONAR --</option>
{todasLasEstaciones.map(e => <option key={e} value={e}>{e}</option>)}
</select>
<div style={{backgroundColor: '#161616', padding: '15px', borderRadius: '4px', marginTop: '10px'}}>
<label style={{display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontSize: '0.8rem', color: '#ccc'}}>
<input type="checkbox" checked={filtros.inversion} onChange={(e) => setFiltros({...filtros, inversion: e.target.checked})} />
INVERSIÓN DE MARCHA (Doble Paridad)
</label>
{pagina === 'especiales' ? (
<label style={{display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontSize: '0.8rem', color: '#00d1b2', marginTop: '10px'}}>
<input type="checkbox" onChange={(e) => setFiltros({...filtros, ultHora: e.target.checked})} />
CANAL ÚLTIMA HORA (Prefijo 90)
</label>
) : (
<select style={{...estilos.select, marginTop: '10px'}} onChange={(e) => setFiltros({...filtros, operador: e.target.value})}>
<option value="Renfe Mercancías">Renfe Mercancías (5)</option>
<option value="Privada">Empresa Privada (6)</option>
<option value="Otra">Otra Operadora (8)</option>
</select>
)}
</div>
<button
style={{...estilos.btnMenu(pagina === 'especiales' ? '#00d1b2' : '#3498db'), backgroundColor: '#eee', color: '#000', marginTop: '20px'}}
onClick={() => generarPeticion(pagina === 'especiales')}
>
GENERAR Y REGISTRAR
</button>
<div style={estilos.display}>{resultado}</div>
<div style={estilos.footer}>
<span>STATUS: ONLINE</span>
<span>SISTEMA DE CIRCULACIÓN CGL</span>
</div>
</div>
)}
</div>
</div>
);
}

9
src/main.jsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

10
vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 3000
}
})