Base Principal del Proyecto
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://checklist_user:checklist_pass_2024@localhost:5432/checklist_db
|
||||
|
||||
# Backend
|
||||
SECRET_KEY=your-super-secret-key-min-32-characters-change-this
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=10080
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=development
|
||||
|
||||
# Frontend
|
||||
VITE_API_URL=http://localhost:8000
|
||||
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
dist/
|
||||
dist-ssr/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.png
|
||||
*.gif
|
||||
*.pdf
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
386
README.md
Normal file
386
README.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Sistema de Checklists Inteligentes - MVP
|
||||
|
||||
Sistema completo de gestión de checklists para talleres mecánicos con integración de IA.
|
||||
|
||||
## 🚀 Características
|
||||
|
||||
- ✅ Gestión de checklists dinámicos (Admin)
|
||||
- ✅ Ejecución de inspecciones (Mecánico)
|
||||
- ✅ Sistema de puntuación automática
|
||||
- ✅ Upload de múltiples fotos
|
||||
- ✅ Firma digital
|
||||
- ✅ Generación de PDF profesional
|
||||
- ✅ API REST completa
|
||||
- ✅ Autenticación JWT
|
||||
- ✅ Base de datos PostgreSQL
|
||||
|
||||
## 📋 Requisitos Previos
|
||||
|
||||
- Docker & Docker Compose instalados
|
||||
- OpenAI API Key (opcional, solo para análisis IA)
|
||||
|
||||
## 🛠️ Instalación Rápida
|
||||
|
||||
### 1. Clonar/Descargar el proyecto
|
||||
|
||||
```bash
|
||||
cd checklist-mvp
|
||||
```
|
||||
|
||||
### 2. Configurar variables de entorno
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Editar `.env` y configurar:
|
||||
- `OPENAI_API_KEY` (si deseas usar análisis IA)
|
||||
- Cambiar `SECRET_KEY` por una clave segura
|
||||
|
||||
### 3. Levantar servicios con Docker
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Esto levantará:
|
||||
- PostgreSQL en puerto 5432
|
||||
- Backend (FastAPI) en puerto 8000
|
||||
- Frontend (React) en puerto 5173
|
||||
|
||||
### 4. Crear usuario administrador
|
||||
|
||||
```bash
|
||||
docker-compose exec backend python -c "
|
||||
from app.core.database import SessionLocal
|
||||
from app.models import User
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
db = SessionLocal()
|
||||
admin = User(
|
||||
username='admin',
|
||||
password_hash=get_password_hash('admin123'),
|
||||
role='admin',
|
||||
email='admin@taller.com',
|
||||
full_name='Administrador'
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
print('Usuario admin creado: admin / admin123')
|
||||
"
|
||||
```
|
||||
|
||||
### 5. Crear usuario mecánico
|
||||
|
||||
```bash
|
||||
docker-compose exec backend python -c "
|
||||
from app.core.database import SessionLocal
|
||||
from app.models import User
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
db = SessionLocal()
|
||||
mechanic = User(
|
||||
username='mecanico1',
|
||||
password_hash=get_password_hash('mecanico123'),
|
||||
role='mechanic',
|
||||
email='mecanico@taller.com',
|
||||
full_name='Juan Pérez'
|
||||
)
|
||||
db.add(mechanic)
|
||||
db.commit()
|
||||
print('Usuario mecánico creado: mecanico1 / mecanico123')
|
||||
"
|
||||
```
|
||||
|
||||
## 🌐 Acceso a la Aplicación
|
||||
|
||||
- **Frontend**: http://localhost:5173
|
||||
- **API Backend**: http://localhost:8000
|
||||
- **Documentación API**: http://localhost:8000/docs
|
||||
|
||||
## 👥 Usuarios de Prueba
|
||||
|
||||
| Usuario | Contraseña | Rol |
|
||||
|---------|-----------|-----|
|
||||
| admin | admin123 | Administrador |
|
||||
| mecanico1 | mecanico123 | Mecánico |
|
||||
|
||||
## 📱 Flujo de Uso
|
||||
|
||||
### Como Administrador:
|
||||
|
||||
1. Login con `admin / admin123`
|
||||
2. Ir a "Checklists"
|
||||
3. Crear nuevo checklist
|
||||
4. Agregar preguntas con tipos:
|
||||
- pass_fail: Revisado / En mal estado
|
||||
- good_bad: Buen estado / Mal estado
|
||||
- text: Respuesta libre
|
||||
- numeric: Números (KM, porcentajes)
|
||||
5. Configurar puntos por pregunta
|
||||
6. Activar el checklist
|
||||
|
||||
### Como Mecánico:
|
||||
|
||||
1. Login con `mecanico1 / mecanico123`
|
||||
2. Ir a "Nueva Inspección"
|
||||
3. Seleccionar checklist activo
|
||||
4. Ingresar datos del vehículo (OR, matrícula, KM)
|
||||
5. Completar preguntas paso a paso
|
||||
6. Subir fotos (máx 3 por pregunta)
|
||||
7. Ver score en tiempo real
|
||||
8. Firmar digitalmente
|
||||
9. Finalizar inspección
|
||||
10. Descargar PDF generado
|
||||
|
||||
## 🗂️ Estructura del Proyecto
|
||||
|
||||
```
|
||||
checklist-mvp/
|
||||
├── docker-compose.yml # Orquestación de servicios
|
||||
├── .env.example # Variables de entorno
|
||||
│
|
||||
├── backend/ # API FastAPI
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── app/
|
||||
│ ├── main.py # Endpoints principales
|
||||
│ ├── models.py # Modelos SQLAlchemy
|
||||
│ ├── schemas.py # Schemas Pydantic
|
||||
│ └── core/
|
||||
│ ├── config.py # Configuración
|
||||
│ ├── database.py # Conexión BD
|
||||
│ └── security.py # JWT & Hashing
|
||||
│
|
||||
└── frontend/ # React + Vite
|
||||
├── Dockerfile
|
||||
├── package.json
|
||||
├── vite.config.js
|
||||
├── tailwind.config.js
|
||||
└── src/
|
||||
├── App.jsx # App principal
|
||||
├── pages/ # Páginas
|
||||
├── components/ # Componentes
|
||||
└── services/ # API calls
|
||||
```
|
||||
|
||||
## 🔌 API Endpoints Principales
|
||||
|
||||
### Autenticación
|
||||
```
|
||||
POST /api/auth/register # Registrar usuario
|
||||
POST /api/auth/login # Login
|
||||
GET /api/auth/me # Usuario actual
|
||||
```
|
||||
|
||||
### Checklists
|
||||
```
|
||||
GET /api/checklists # Listar checklists
|
||||
POST /api/checklists # Crear checklist (admin)
|
||||
GET /api/checklists/{id} # Ver detalle con preguntas
|
||||
PUT /api/checklists/{id} # Actualizar
|
||||
```
|
||||
|
||||
### Preguntas
|
||||
```
|
||||
POST /api/questions # Crear pregunta (admin)
|
||||
PUT /api/questions/{id} # Actualizar
|
||||
DELETE /api/questions/{id} # Eliminar
|
||||
```
|
||||
|
||||
### Inspecciones
|
||||
```
|
||||
GET /api/inspections # Listar inspecciones
|
||||
POST /api/inspections # Crear inspección
|
||||
GET /api/inspections/{id} # Ver detalle completo
|
||||
PUT /api/inspections/{id} # Actualizar (guardar borrador)
|
||||
POST /api/inspections/{id}/complete # Finalizar
|
||||
```
|
||||
|
||||
### Respuestas
|
||||
```
|
||||
POST /api/answers # Crear respuesta
|
||||
PUT /api/answers/{id} # Actualizar respuesta
|
||||
```
|
||||
|
||||
### Archivos
|
||||
```
|
||||
POST /api/answers/{id}/upload # Subir foto
|
||||
```
|
||||
|
||||
## 🗄️ Modelo de Datos
|
||||
|
||||
### Users
|
||||
- id, username, email, password_hash
|
||||
- role (admin/mechanic)
|
||||
- full_name, is_active
|
||||
|
||||
### Checklists
|
||||
- id, name, description
|
||||
- ai_mode (off/assisted/copilot)
|
||||
- scoring_enabled, max_score
|
||||
- is_active, created_by
|
||||
|
||||
### Questions
|
||||
- id, checklist_id, section
|
||||
- text, type, points
|
||||
- options (JSON), order
|
||||
- allow_photos, max_photos
|
||||
|
||||
### Inspections
|
||||
- id, checklist_id, mechanic_id
|
||||
- or_number, vehicle_plate, vehicle_km
|
||||
- score, max_score, percentage
|
||||
- flagged_items_count
|
||||
- status (draft/completed)
|
||||
- signature_data
|
||||
|
||||
### Answers
|
||||
- id, inspection_id, question_id
|
||||
- answer_value, status (ok/warning/critical)
|
||||
- points_earned, comment
|
||||
- is_flagged, ai_analysis (JSON)
|
||||
|
||||
### MediaFiles
|
||||
- id, answer_id
|
||||
- file_path, file_type, caption
|
||||
|
||||
## 🎨 Tipos de Preguntas
|
||||
|
||||
| Tipo | Descripción | Opciones |
|
||||
|------|-------------|----------|
|
||||
| pass_fail | Pasa/Falla | REVISADO / EN MAL ESTADO |
|
||||
| good_bad | Estado | Buen estado / Mal estado |
|
||||
| status | Acción | REVISADO / SUSTITUIDO / Ninguna acción |
|
||||
| text | Texto libre | - |
|
||||
| numeric | Número | - |
|
||||
| date | Fecha | - |
|
||||
| multiple_choice | Opciones | Configurables |
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
- Autenticación JWT con tokens Bearer
|
||||
- Passwords hasheados con bcrypt
|
||||
- CORS configurado para desarrollo
|
||||
- Validación con Pydantic
|
||||
- Autorización por roles (admin/mechanic)
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
### Ver logs del backend
|
||||
```bash
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
### Ver logs del frontend
|
||||
```bash
|
||||
docker-compose logs -f frontend
|
||||
```
|
||||
|
||||
### Ver logs de PostgreSQL
|
||||
```bash
|
||||
docker-compose logs -f postgres
|
||||
```
|
||||
|
||||
### Acceder a la base de datos
|
||||
```bash
|
||||
docker-compose exec postgres psql -U checklist_user -d checklist_db
|
||||
```
|
||||
|
||||
### Reiniciar servicios
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Detener todo
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Eliminar volúmenes (resetear BD)
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## 📝 Crear Checklist de Ejemplo
|
||||
|
||||
Puedes usar la API directamente o crear via SQL:
|
||||
|
||||
```sql
|
||||
-- Conectar a la BD
|
||||
docker-compose exec postgres psql -U checklist_user -d checklist_db
|
||||
|
||||
-- Insertar checklist
|
||||
INSERT INTO checklists (name, description, ai_mode, scoring_enabled, max_score, is_active, created_by)
|
||||
VALUES ('Mantenimiento Básico', 'Checklist de mantenimiento estándar', 'assisted', true, 0, true, 1);
|
||||
|
||||
-- Insertar preguntas
|
||||
INSERT INTO questions (checklist_id, section, text, type, points, order, allow_photos) VALUES
|
||||
(1, 'Sistema Eléctrico', 'Estado de la batería', 'good_bad', 1, 1, true),
|
||||
(1, 'Sistema Eléctrico', 'Bocina', 'pass_fail', 1, 2, false),
|
||||
(1, 'Frenos', 'Frenos (pastillas, discos)', 'pass_fail', 2, 3, true),
|
||||
(1, 'Motor', 'Nivel de aceite', 'pass_fail', 1, 4, true),
|
||||
(1, 'Motor', 'Fugas de aceite', 'pass_fail', 2, 5, true);
|
||||
|
||||
-- Actualizar max_score del checklist
|
||||
UPDATE checklists SET max_score = (
|
||||
SELECT SUM(points) FROM questions WHERE checklist_id = 1
|
||||
) WHERE id = 1;
|
||||
```
|
||||
|
||||
## 🚀 Despliegue en Producción
|
||||
|
||||
### Cambios necesarios para producción:
|
||||
|
||||
1. **Variables de entorno**:
|
||||
- Cambiar `SECRET_KEY` por una clave segura aleatoria
|
||||
- Configurar `OPENAI_API_KEY`
|
||||
- Usar base de datos PostgreSQL externa
|
||||
|
||||
2. **Frontend**:
|
||||
- Cambiar `VITE_API_URL` a URL del backend en producción
|
||||
- Ejecutar `npm run build`
|
||||
- Servir carpeta `dist/` con Nginx
|
||||
|
||||
3. **Backend**:
|
||||
- Configurar CORS con dominio específico
|
||||
- Usar Gunicorn en lugar de Uvicorn
|
||||
- Configurar HTTPS con certificado SSL
|
||||
|
||||
4. **Base de datos**:
|
||||
- Usar PostgreSQL gestionado (AWS RDS, DigitalOcean, etc)
|
||||
- Hacer backups automáticos
|
||||
- Configurar conexiones SSL
|
||||
|
||||
5. **Storage**:
|
||||
- Usar S3 o similar para archivos
|
||||
- Configurar CDN para imágenes
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
MIT License - Uso libre para proyectos comerciales y personales
|
||||
|
||||
## 🆘 Soporte
|
||||
|
||||
Para problemas o preguntas:
|
||||
1. Revisar logs con `docker-compose logs`
|
||||
2. Verificar que todos los servicios están corriendo con `docker-compose ps`
|
||||
3. Revisar documentación de API en http://localhost:8000/docs
|
||||
|
||||
## 🎯 Próximos Pasos (Post-MVP)
|
||||
|
||||
- [ ] Lógica condicional en preguntas
|
||||
- [ ] Modo Copiloto IA completo
|
||||
- [ ] Análisis avanzado de imágenes
|
||||
- [ ] Dashboard de estadísticas
|
||||
- [ ] Historial de vehículos
|
||||
- [ ] Notificaciones por email/WhatsApp
|
||||
- [ ] App móvil React Native
|
||||
- [ ] Multi-tenant para varios talleres
|
||||
- [ ] Integración con sistemas ERP
|
||||
|
||||
---
|
||||
|
||||
**¡Listo para usar! 🎉**
|
||||
|
||||
Recuerda cambiar las contraseñas por defecto antes de usar en producción.
|
||||
27
backend/Dockerfile
Normal file
27
backend/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias del sistema
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copiar requirements
|
||||
COPY requirements.txt .
|
||||
|
||||
# Instalar dependencias de Python
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copiar código
|
||||
COPY . .
|
||||
|
||||
# Crear directorio para uploads
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 8000
|
||||
|
||||
# Comando por defecto
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Backend app package
|
||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core package
|
||||
27
backend/app/core/config.py
Normal file
27
backend/app/core/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Database
|
||||
DATABASE_URL: str
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080 # 7 días
|
||||
|
||||
# OpenAI
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT: str = "development"
|
||||
|
||||
# Uploads
|
||||
UPLOAD_DIR: str = "uploads"
|
||||
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
settings = Settings()
|
||||
21
backend/app/core/database.py
Normal file
21
backend/app/core/database.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
echo=settings.ENVIRONMENT == "development"
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
30
backend/app/core/security.py
Normal file
30
backend/app/core/security.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def decode_access_token(token: str):
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
454
backend/app/main.py
Normal file
454
backend/app/main.py
Normal file
@@ -0,0 +1,454 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from typing import List
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.core.database import engine, get_db, Base
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token, decode_access_token
|
||||
from app import models, schemas
|
||||
|
||||
# Crear tablas
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(title="Checklist Inteligente API", version="1.0.0")
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
# Dependency para obtener usuario actual
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token inválido o expirado"
|
||||
)
|
||||
|
||||
user = db.query(models.User).filter(models.User.id == payload.get("sub")).first()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||
|
||||
return user
|
||||
|
||||
|
||||
# ============= AUTH ENDPOINTS =============
|
||||
@app.post("/api/auth/register", response_model=schemas.User)
|
||||
def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
# Verificar si usuario existe
|
||||
db_user = db.query(models.User).filter(models.User.username == user.username).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Usuario ya existe")
|
||||
|
||||
# Crear usuario
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = models.User(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
role=user.role,
|
||||
password_hash=hashed_password
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
@app.post("/api/auth/login", response_model=schemas.Token)
|
||||
def login(user_login: schemas.UserLogin, db: Session = Depends(get_db)):
|
||||
user = db.query(models.User).filter(models.User.username == user_login.username).first()
|
||||
if not user or not verify_password(user_login.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Usuario o contraseña incorrectos"
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": user.id, "role": user.role})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/auth/me", response_model=schemas.User)
|
||||
def get_me(current_user: models.User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
# ============= CHECKLIST ENDPOINTS =============
|
||||
@app.get("/api/checklists", response_model=List[schemas.Checklist])
|
||||
def get_checklists(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
query = db.query(models.Checklist)
|
||||
if active_only:
|
||||
query = query.filter(models.Checklist.is_active == True)
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@app.get("/api/checklists/{checklist_id}", response_model=schemas.ChecklistWithQuestions)
|
||||
def get_checklist(checklist_id: int, db: Session = Depends(get_db)):
|
||||
checklist = db.query(models.Checklist).options(
|
||||
joinedload(models.Checklist.questions)
|
||||
).filter(models.Checklist.id == checklist_id).first()
|
||||
|
||||
if not checklist:
|
||||
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
||||
|
||||
return checklist
|
||||
|
||||
|
||||
@app.post("/api/checklists", response_model=schemas.Checklist)
|
||||
def create_checklist(
|
||||
checklist: schemas.ChecklistCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="No autorizado")
|
||||
|
||||
db_checklist = models.Checklist(**checklist.dict(), created_by=current_user.id)
|
||||
db.add(db_checklist)
|
||||
db.commit()
|
||||
db.refresh(db_checklist)
|
||||
return db_checklist
|
||||
|
||||
|
||||
@app.put("/api/checklists/{checklist_id}", response_model=schemas.Checklist)
|
||||
def update_checklist(
|
||||
checklist_id: int,
|
||||
checklist: schemas.ChecklistUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="No autorizado")
|
||||
|
||||
db_checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first()
|
||||
if not db_checklist:
|
||||
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
||||
|
||||
for key, value in checklist.dict(exclude_unset=True).items():
|
||||
setattr(db_checklist, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_checklist)
|
||||
return db_checklist
|
||||
|
||||
|
||||
# ============= QUESTION ENDPOINTS =============
|
||||
@app.post("/api/questions", response_model=schemas.Question)
|
||||
def create_question(
|
||||
question: schemas.QuestionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="No autorizado")
|
||||
|
||||
db_question = models.Question(**question.dict())
|
||||
db.add(db_question)
|
||||
|
||||
# Actualizar max_score del checklist
|
||||
checklist = db.query(models.Checklist).filter(
|
||||
models.Checklist.id == question.checklist_id
|
||||
).first()
|
||||
if checklist:
|
||||
checklist.max_score += question.points
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_question)
|
||||
return db_question
|
||||
|
||||
|
||||
@app.put("/api/questions/{question_id}", response_model=schemas.Question)
|
||||
def update_question(
|
||||
question_id: int,
|
||||
question: schemas.QuestionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="No autorizado")
|
||||
|
||||
db_question = db.query(models.Question).filter(models.Question.id == question_id).first()
|
||||
if not db_question:
|
||||
raise HTTPException(status_code=404, detail="Pregunta no encontrada")
|
||||
|
||||
for key, value in question.dict(exclude_unset=True).items():
|
||||
setattr(db_question, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_question)
|
||||
return db_question
|
||||
|
||||
|
||||
@app.delete("/api/questions/{question_id}")
|
||||
def delete_question(
|
||||
question_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="No autorizado")
|
||||
|
||||
db_question = db.query(models.Question).filter(models.Question.id == question_id).first()
|
||||
if not db_question:
|
||||
raise HTTPException(status_code=404, detail="Pregunta no encontrada")
|
||||
|
||||
db.delete(db_question)
|
||||
db.commit()
|
||||
return {"message": "Pregunta eliminada"}
|
||||
|
||||
|
||||
# ============= INSPECTION ENDPOINTS =============
|
||||
@app.get("/api/inspections", response_model=List[schemas.Inspection])
|
||||
def get_inspections(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
vehicle_plate: str = None,
|
||||
status: str = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
query = db.query(models.Inspection)
|
||||
|
||||
# Mecánicos solo ven sus inspecciones
|
||||
if current_user.role == "mechanic":
|
||||
query = query.filter(models.Inspection.mechanic_id == current_user.id)
|
||||
|
||||
if vehicle_plate:
|
||||
query = query.filter(models.Inspection.vehicle_plate.contains(vehicle_plate))
|
||||
|
||||
if status:
|
||||
query = query.filter(models.Inspection.status == status)
|
||||
|
||||
return query.order_by(models.Inspection.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@app.get("/api/inspections/{inspection_id}", response_model=schemas.InspectionDetail)
|
||||
def get_inspection(
|
||||
inspection_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
inspection = db.query(models.Inspection).options(
|
||||
joinedload(models.Inspection.checklist),
|
||||
joinedload(models.Inspection.mechanic),
|
||||
joinedload(models.Inspection.answers).joinedload(models.Answer.question),
|
||||
joinedload(models.Inspection.answers).joinedload(models.Answer.media_files)
|
||||
).filter(models.Inspection.id == inspection_id).first()
|
||||
|
||||
if not inspection:
|
||||
raise HTTPException(status_code=404, detail="Inspección no encontrada")
|
||||
|
||||
return inspection
|
||||
|
||||
|
||||
@app.post("/api/inspections", response_model=schemas.Inspection)
|
||||
def create_inspection(
|
||||
inspection: schemas.InspectionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
# Obtener max_score del checklist
|
||||
checklist = db.query(models.Checklist).filter(
|
||||
models.Checklist.id == inspection.checklist_id
|
||||
).first()
|
||||
|
||||
if not checklist:
|
||||
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
||||
|
||||
db_inspection = models.Inspection(
|
||||
**inspection.dict(),
|
||||
mechanic_id=current_user.id,
|
||||
max_score=checklist.max_score
|
||||
)
|
||||
db.add(db_inspection)
|
||||
db.commit()
|
||||
db.refresh(db_inspection)
|
||||
return db_inspection
|
||||
|
||||
|
||||
@app.put("/api/inspections/{inspection_id}", response_model=schemas.Inspection)
|
||||
def update_inspection(
|
||||
inspection_id: int,
|
||||
inspection: schemas.InspectionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
db_inspection = db.query(models.Inspection).filter(
|
||||
models.Inspection.id == inspection_id
|
||||
).first()
|
||||
|
||||
if not db_inspection:
|
||||
raise HTTPException(status_code=404, detail="Inspección no encontrada")
|
||||
|
||||
for key, value in inspection.dict(exclude_unset=True).items():
|
||||
setattr(db_inspection, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_inspection)
|
||||
return db_inspection
|
||||
|
||||
|
||||
@app.post("/api/inspections/{inspection_id}/complete", response_model=schemas.Inspection)
|
||||
def complete_inspection(
|
||||
inspection_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
inspection = db.query(models.Inspection).filter(
|
||||
models.Inspection.id == inspection_id
|
||||
).first()
|
||||
|
||||
if not inspection:
|
||||
raise HTTPException(status_code=404, detail="Inspección no encontrada")
|
||||
|
||||
# Calcular score
|
||||
answers = db.query(models.Answer).filter(models.Answer.inspection_id == inspection_id).all()
|
||||
total_score = sum(a.points_earned for a in answers)
|
||||
flagged_count = sum(1 for a in answers if a.is_flagged)
|
||||
|
||||
inspection.score = total_score
|
||||
inspection.percentage = (total_score / inspection.max_score * 100) if inspection.max_score > 0 else 0
|
||||
inspection.flagged_items_count = flagged_count
|
||||
inspection.status = "completed"
|
||||
inspection.completed_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(inspection)
|
||||
return inspection
|
||||
|
||||
|
||||
# ============= ANSWER ENDPOINTS =============
|
||||
@app.post("/api/answers", response_model=schemas.Answer)
|
||||
def create_answer(
|
||||
answer: schemas.AnswerCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
# Obtener la pregunta para saber los puntos
|
||||
question = db.query(models.Question).filter(models.Question.id == answer.question_id).first()
|
||||
|
||||
if not question:
|
||||
raise HTTPException(status_code=404, detail="Pregunta no encontrada")
|
||||
|
||||
# Calcular puntos según status
|
||||
points_earned = 0
|
||||
if answer.status == "ok":
|
||||
points_earned = question.points
|
||||
elif answer.status == "warning":
|
||||
points_earned = int(question.points * 0.5)
|
||||
|
||||
db_answer = models.Answer(
|
||||
**answer.dict(),
|
||||
points_earned=points_earned
|
||||
)
|
||||
db.add(db_answer)
|
||||
db.commit()
|
||||
db.refresh(db_answer)
|
||||
return db_answer
|
||||
|
||||
|
||||
@app.put("/api/answers/{answer_id}", response_model=schemas.Answer)
|
||||
def update_answer(
|
||||
answer_id: int,
|
||||
answer: schemas.AnswerUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
db_answer = db.query(models.Answer).filter(models.Answer.id == answer_id).first()
|
||||
|
||||
if not db_answer:
|
||||
raise HTTPException(status_code=404, detail="Respuesta no encontrada")
|
||||
|
||||
# Recalcular puntos si cambió el status
|
||||
if answer.status and answer.status != db_answer.status:
|
||||
question = db.query(models.Question).filter(
|
||||
models.Question.id == db_answer.question_id
|
||||
).first()
|
||||
|
||||
if answer.status == "ok":
|
||||
db_answer.points_earned = question.points
|
||||
elif answer.status == "warning":
|
||||
db_answer.points_earned = int(question.points * 0.5)
|
||||
else:
|
||||
db_answer.points_earned = 0
|
||||
|
||||
for key, value in answer.dict(exclude_unset=True).items():
|
||||
setattr(db_answer, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_answer)
|
||||
return db_answer
|
||||
|
||||
|
||||
# ============= MEDIA FILE ENDPOINTS =============
|
||||
@app.post("/api/answers/{answer_id}/upload", response_model=schemas.MediaFile)
|
||||
async def upload_photo(
|
||||
answer_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
# Verificar que la respuesta existe
|
||||
answer = db.query(models.Answer).filter(models.Answer.id == answer_id).first()
|
||||
if not answer:
|
||||
raise HTTPException(status_code=404, detail="Respuesta no encontrada")
|
||||
|
||||
# Crear directorio si no existe
|
||||
upload_dir = f"uploads/inspection_{answer.inspection_id}"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
# Guardar archivo
|
||||
file_extension = file.filename.split(".")[-1]
|
||||
file_name = f"answer_{answer_id}_{datetime.now().timestamp()}.{file_extension}"
|
||||
file_path = os.path.join(upload_dir, file_name)
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Crear registro en BD
|
||||
media_file = models.MediaFile(
|
||||
answer_id=answer_id,
|
||||
file_path=file_path,
|
||||
file_type="image"
|
||||
)
|
||||
db.add(media_file)
|
||||
db.commit()
|
||||
db.refresh(media_file)
|
||||
|
||||
return media_file
|
||||
|
||||
|
||||
# ============= HEALTH CHECK =============
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {"message": "Checklist Inteligente API", "version": "1.0.0", "status": "running"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {"status": "healthy"}
|
||||
146
backend/app/models.py
Normal file
146
backend/app/models.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
email = Column(String(100), unique=True, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
role = Column(String(20), nullable=False) # admin, mechanic
|
||||
full_name = Column(String(100))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
checklists_created = relationship("Checklist", back_populates="creator")
|
||||
inspections = relationship("Inspection", back_populates="mechanic")
|
||||
|
||||
|
||||
class Checklist(Base):
|
||||
__tablename__ = "checklists"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(200), nullable=False)
|
||||
description = Column(Text)
|
||||
ai_mode = Column(String(20), default="off") # off, assisted, copilot
|
||||
scoring_enabled = Column(Boolean, default=True)
|
||||
max_score = Column(Integer, default=0)
|
||||
logo_url = Column(String(500))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User", back_populates="checklists_created")
|
||||
questions = relationship("Question", back_populates="checklist", cascade="all, delete-orphan")
|
||||
inspections = relationship("Inspection", back_populates="checklist")
|
||||
|
||||
|
||||
class Question(Base):
|
||||
__tablename__ = "questions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False)
|
||||
section = Column(String(100)) # Sistema eléctrico, Frenos, etc
|
||||
text = Column(Text, nullable=False)
|
||||
type = Column(String(30), nullable=False) # pass_fail, good_bad, text, etc
|
||||
points = Column(Integer, default=1)
|
||||
options = Column(JSON) # Para multiple choice
|
||||
order = Column(Integer, default=0)
|
||||
allow_photos = Column(Boolean, default=True)
|
||||
max_photos = Column(Integer, default=3)
|
||||
requires_comment_on_fail = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
checklist = relationship("Checklist", back_populates="questions")
|
||||
answers = relationship("Answer", back_populates="question")
|
||||
|
||||
|
||||
class Inspection(Base):
|
||||
__tablename__ = "inspections"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False)
|
||||
mechanic_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Datos de la OR
|
||||
or_number = Column(String(50))
|
||||
work_order_number = Column(String(50))
|
||||
|
||||
# Datos del vehículo
|
||||
vehicle_plate = Column(String(20), nullable=False, index=True)
|
||||
vehicle_brand = Column(String(50))
|
||||
vehicle_model = Column(String(100))
|
||||
vehicle_km = Column(Integer)
|
||||
client_name = Column(String(200))
|
||||
|
||||
# Scoring
|
||||
score = Column(Integer, default=0)
|
||||
max_score = Column(Integer, default=0)
|
||||
percentage = Column(Float, default=0.0)
|
||||
flagged_items_count = Column(Integer, default=0)
|
||||
|
||||
# Estado
|
||||
status = Column(String(20), default="draft") # draft, completed
|
||||
|
||||
# Firma
|
||||
signature_data = Column(Text) # Base64 de la firma
|
||||
signed_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Timestamps
|
||||
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
completed_at = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
checklist = relationship("Checklist", back_populates="inspections")
|
||||
mechanic = relationship("User", back_populates="inspections")
|
||||
answers = relationship("Answer", back_populates="inspection", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Answer(Base):
|
||||
__tablename__ = "answers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
inspection_id = Column(Integer, ForeignKey("inspections.id"), nullable=False)
|
||||
question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
|
||||
|
||||
answer_value = Column(Text) # La respuesta del mecánico
|
||||
status = Column(String(20), default="ok") # ok, warning, critical, info
|
||||
points_earned = Column(Integer, default=0)
|
||||
comment = Column(Text) # Comentarios adicionales
|
||||
|
||||
ai_analysis = Column(JSON) # Análisis de IA si aplica
|
||||
is_flagged = Column(Boolean, default=False) # Si requiere atención
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
inspection = relationship("Inspection", back_populates="answers")
|
||||
question = relationship("Question", back_populates="answers")
|
||||
media_files = relationship("MediaFile", back_populates="answer", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MediaFile(Base):
|
||||
__tablename__ = "media_files"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
answer_id = Column(Integer, ForeignKey("answers.id"), nullable=False)
|
||||
|
||||
file_path = Column(String(500), nullable=False)
|
||||
file_type = Column(String(20), default="image") # image, video
|
||||
caption = Column(Text)
|
||||
order = Column(Integer, default=0)
|
||||
|
||||
uploaded_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
answer = relationship("Answer", back_populates="media_files")
|
||||
177
backend/app/schemas.py
Normal file
177
backend/app/schemas.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
# User Schemas
|
||||
class UserBase(BaseModel):
|
||||
username: str
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = None
|
||||
role: str = "mechanic"
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
user: User
|
||||
|
||||
|
||||
# Checklist Schemas
|
||||
class ChecklistBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
ai_mode: str = "off"
|
||||
scoring_enabled: bool = True
|
||||
logo_url: Optional[str] = None
|
||||
|
||||
class ChecklistCreate(ChecklistBase):
|
||||
pass
|
||||
|
||||
class ChecklistUpdate(ChecklistBase):
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Checklist(ChecklistBase):
|
||||
id: int
|
||||
max_score: int
|
||||
is_active: bool
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Question Schemas
|
||||
class QuestionBase(BaseModel):
|
||||
section: Optional[str] = None
|
||||
text: str
|
||||
type: str
|
||||
points: int = 1
|
||||
options: Optional[dict] = None
|
||||
order: int = 0
|
||||
allow_photos: bool = True
|
||||
max_photos: int = 3
|
||||
requires_comment_on_fail: bool = False
|
||||
|
||||
class QuestionCreate(QuestionBase):
|
||||
checklist_id: int
|
||||
|
||||
class QuestionUpdate(QuestionBase):
|
||||
pass
|
||||
|
||||
class Question(QuestionBase):
|
||||
id: int
|
||||
checklist_id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Inspection Schemas
|
||||
class InspectionBase(BaseModel):
|
||||
or_number: Optional[str] = None
|
||||
work_order_number: Optional[str] = None
|
||||
vehicle_plate: str
|
||||
vehicle_brand: Optional[str] = None
|
||||
vehicle_model: Optional[str] = None
|
||||
vehicle_km: Optional[int] = None
|
||||
client_name: Optional[str] = None
|
||||
|
||||
class InspectionCreate(InspectionBase):
|
||||
checklist_id: int
|
||||
|
||||
class InspectionUpdate(BaseModel):
|
||||
vehicle_brand: Optional[str] = None
|
||||
vehicle_model: Optional[str] = None
|
||||
vehicle_km: Optional[int] = None
|
||||
signature_data: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
class Inspection(InspectionBase):
|
||||
id: int
|
||||
checklist_id: int
|
||||
mechanic_id: int
|
||||
score: int
|
||||
max_score: int
|
||||
percentage: float
|
||||
flagged_items_count: int
|
||||
status: str
|
||||
started_at: datetime
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Answer Schemas
|
||||
class AnswerBase(BaseModel):
|
||||
answer_value: str
|
||||
status: str = "ok"
|
||||
comment: Optional[str] = None
|
||||
is_flagged: bool = False
|
||||
|
||||
class AnswerCreate(AnswerBase):
|
||||
inspection_id: int
|
||||
question_id: int
|
||||
|
||||
class AnswerUpdate(AnswerBase):
|
||||
pass
|
||||
|
||||
class Answer(AnswerBase):
|
||||
id: int
|
||||
inspection_id: int
|
||||
question_id: int
|
||||
points_earned: int
|
||||
ai_analysis: Optional[dict] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# MediaFile Schemas
|
||||
class MediaFileBase(BaseModel):
|
||||
caption: Optional[str] = None
|
||||
order: int = 0
|
||||
|
||||
class MediaFileCreate(MediaFileBase):
|
||||
file_type: str = "image"
|
||||
|
||||
class MediaFile(MediaFileBase):
|
||||
id: int
|
||||
answer_id: int
|
||||
file_path: str
|
||||
file_type: str
|
||||
uploaded_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Response Schemas
|
||||
class ChecklistWithQuestions(Checklist):
|
||||
questions: List[Question] = []
|
||||
|
||||
class InspectionDetail(Inspection):
|
||||
checklist: Checklist
|
||||
mechanic: User
|
||||
answers: List[Answer] = []
|
||||
|
||||
class AnswerWithMedia(Answer):
|
||||
media_files: List[MediaFile] = []
|
||||
question: Question
|
||||
14
backend/requirements.txt
Normal file
14
backend/requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
sqlalchemy==2.0.25
|
||||
alembic==1.13.1
|
||||
psycopg2-binary==2.9.9
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.6
|
||||
openai==1.10.0
|
||||
Pillow==10.2.0
|
||||
reportlab==4.0.9
|
||||
python-dotenv==1.0.0
|
||||
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: checklist-db
|
||||
environment:
|
||||
POSTGRES_DB: checklist_db
|
||||
POSTGRES_USER: checklist_user
|
||||
POSTGRES_PASSWORD: checklist_pass_2024
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U checklist_user"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: checklist-backend
|
||||
environment:
|
||||
DATABASE_URL: postgresql://checklist_user:checklist_pass_2024@postgres:5432/checklist_db
|
||||
SECRET_KEY: your-secret-key-change-in-production-min-32-chars
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ENVIRONMENT: development
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./uploads:/app/uploads
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: checklist-frontend
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:8000
|
||||
command: npm run dev -- --host
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Instalar dependencias
|
||||
RUN npm install
|
||||
|
||||
# Copiar código
|
||||
COPY . .
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 5173
|
||||
|
||||
# Comando por defecto
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Checklist Inteligente - Sistema de Inspecciones</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "checklist-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"axios": "^1.6.5",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"lucide-react": "^0.303.0",
|
||||
"clsx": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.33",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
363
frontend/src/App.jsx
Normal file
363
frontend/src/App.jsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Verificar si hay token guardado
|
||||
const token = localStorage.getItem('token')
|
||||
const userData = localStorage.getItem('user')
|
||||
|
||||
if (token && userData) {
|
||||
setUser(JSON.parse(userData))
|
||||
}
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-xl">Cargando...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{!user ? (
|
||||
<LoginPage setUser={setUser} />
|
||||
) : (
|
||||
<DashboardPage user={user} setUser={setUser} />
|
||||
)}
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
// Componente de Login
|
||||
function LoginPage({ setUser }) {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
const response = await fetch(`${API_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || 'Error al iniciar sesión')
|
||||
}
|
||||
|
||||
// Guardar token y usuario
|
||||
localStorage.setItem('token', data.access_token)
|
||||
localStorage.setItem('user', JSON.stringify(data.user))
|
||||
setUser(data.user)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-500 to-blue-700 px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Checklist Inteligente</h1>
|
||||
<p className="text-gray-600 mt-2">Sistema de Inspecciones</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Usuario
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="admin"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 text-red-600 px-4 py-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Iniciando sesión...' : 'Iniciar Sesión'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<p className="text-sm text-gray-600 text-center">Usuarios de prueba:</p>
|
||||
<div className="mt-2 space-y-1 text-xs text-gray-500 text-center">
|
||||
<p><strong>Admin:</strong> admin / admin123</p>
|
||||
<p><strong>Mecánico:</strong> mecanico1 / mecanico123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Componente Dashboard
|
||||
function DashboardPage({ user, setUser }) {
|
||||
const [checklists, setChecklists] = useState([])
|
||||
const [inspections, setInspections] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState('checklists')
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
// Cargar checklists
|
||||
const checklistsRes = await fetch(`${API_URL}/api/checklists?active_only=true`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const checklistsData = await checklistsRes.json()
|
||||
setChecklists(checklistsData)
|
||||
|
||||
// Cargar inspecciones
|
||||
const inspectionsRes = await fetch(`${API_URL}/api/inspections?limit=10`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const inspectionsData = await inspectionsRes.json()
|
||||
setInspections(inspectionsData)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Checklist Inteligente</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Bienvenido, {user.full_name || user.username} ({user.role === 'admin' ? 'Administrador' : 'Mecánico'})
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
||||
>
|
||||
Cerrar Sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex -mb-px">
|
||||
<button
|
||||
onClick={() => setActiveTab('checklists')}
|
||||
className={`px-6 py-3 text-sm font-medium border-b-2 ${
|
||||
activeTab === 'checklists'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Checklists
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('inspections')}
|
||||
className={`px-6 py-3 text-sm font-medium border-b-2 ${
|
||||
activeTab === 'inspections'
|
||||
: 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Inspecciones
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">Cargando datos...</div>
|
||||
</div>
|
||||
) : activeTab === 'checklists' ? (
|
||||
<ChecklistsTab checklists={checklists} user={user} />
|
||||
) : (
|
||||
<InspectionsTab inspections={inspections} user={user} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6 mb-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 className="text-sm font-medium text-blue-800 mb-2">📘 Información del MVP</h3>
|
||||
<p className="text-sm text-blue-700">
|
||||
Este es el MVP funcional del sistema de checklists inteligentes.
|
||||
El frontend completo con todas las funcionalidades se encuentra en desarrollo.
|
||||
Puedes probar la API completa en: <a href="http://localhost:8000/docs" target="_blank" className="underline font-medium">http://localhost:8000/docs</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChecklistsTab({ checklists, user }) {
|
||||
if (checklists.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No hay checklists activos</p>
|
||||
<p className="text-sm text-gray-400 mt-2">Ejecuta ./init-data.sh para crear datos de ejemplo</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{checklists.map((checklist) => (
|
||||
<div key={checklist.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{checklist.name}</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">{checklist.description}</p>
|
||||
<div className="flex gap-4 mt-3 text-sm">
|
||||
<span className="text-gray-500">
|
||||
Puntuación máxima: <strong>{checklist.max_score}</strong>
|
||||
</span>
|
||||
<span className="text-gray-500">
|
||||
Modo IA: <strong className="capitalize">{checklist.ai_mode}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{user.role === 'mechanic' && (
|
||||
<button className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
|
||||
Nueva Inspección
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InspectionsTab({ inspections, user }) {
|
||||
if (inspections.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No hay inspecciones registradas</p>
|
||||
<p className="text-sm text-gray-400 mt-2">Crea tu primera inspección</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{inspections.map((inspection) => (
|
||||
<div key={inspection.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{inspection.vehicle_plate}
|
||||
</h3>
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
inspection.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{inspection.status === 'completed' ? 'Completada' : 'Borrador'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{inspection.vehicle_brand} {inspection.vehicle_model} - {inspection.vehicle_km} km
|
||||
</p>
|
||||
<div className="flex gap-4 mt-3 text-sm">
|
||||
<span className="text-gray-500">
|
||||
OR: <strong>{inspection.or_number || 'N/A'}</strong>
|
||||
</span>
|
||||
<span className="text-gray-500">
|
||||
Score: <strong>{inspection.score}/{inspection.max_score}</strong> ({inspection.percentage.toFixed(1)}%)
|
||||
</span>
|
||||
{inspection.flagged_items_count > 0 && (
|
||||
<span className="text-red-600">
|
||||
⚠️ <strong>{inspection.flagged_items_count}</strong> elementos señalados
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
||||
Ver Detalle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
17
frontend/src/index.css
Normal file
17
frontend/src/index.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
13
frontend/vite.config.js
Normal file
13
frontend/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
watch: {
|
||||
usePolling: true
|
||||
}
|
||||
}
|
||||
})
|
||||
132
init-data.sh
Normal file
132
init-data.sh
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Inicializando Sistema de Checklists..."
|
||||
echo ""
|
||||
|
||||
# Esperar a que PostgreSQL esté listo
|
||||
echo "⏳ Esperando a que PostgreSQL esté listo..."
|
||||
sleep 5
|
||||
|
||||
# Crear usuarios
|
||||
echo "👥 Creando usuarios de prueba..."
|
||||
|
||||
docker-compose exec -T backend python << EOF
|
||||
from app.core.database import SessionLocal
|
||||
from app.models import User
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Crear admin
|
||||
admin = User(
|
||||
username='admin',
|
||||
password_hash=get_password_hash('admin123'),
|
||||
role='admin',
|
||||
email='admin@taller.com',
|
||||
full_name='Administrador Sistema'
|
||||
)
|
||||
db.add(admin)
|
||||
|
||||
# Crear mecánico
|
||||
mechanic = User(
|
||||
username='mecanico1',
|
||||
password_hash=get_password_hash('mecanico123'),
|
||||
role='mechanic',
|
||||
email='mecanico@taller.com',
|
||||
full_name='Juan Pérez'
|
||||
)
|
||||
db.add(mechanic)
|
||||
|
||||
db.commit()
|
||||
print('✅ Usuarios creados exitosamente')
|
||||
EOF
|
||||
|
||||
# Crear checklist de ejemplo
|
||||
echo "📋 Creando checklist de ejemplo..."
|
||||
|
||||
docker-compose exec -T backend python << EOF
|
||||
from app.core.database import SessionLocal
|
||||
from app.models import Checklist, Question
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Crear checklist
|
||||
checklist = Checklist(
|
||||
name='Mantenimiento Preventivo',
|
||||
description='Checklist estándar de mantenimiento preventivo',
|
||||
ai_mode='assisted',
|
||||
scoring_enabled=True,
|
||||
is_active=True,
|
||||
created_by=1
|
||||
)
|
||||
db.add(checklist)
|
||||
db.commit()
|
||||
db.refresh(checklist)
|
||||
|
||||
# Crear preguntas por sección
|
||||
questions_data = [
|
||||
# Sistema Eléctrico
|
||||
('Sistema Eléctrico', 'Estado de la batería', 'good_bad', 1, True),
|
||||
('Sistema Eléctrico', 'Bocina', 'pass_fail', 1, False),
|
||||
('Sistema Eléctrico', 'Luces (posición, cruce, carretera)', 'pass_fail', 1, False),
|
||||
('Sistema Eléctrico', 'Testigos en cuadro', 'pass_fail', 1, True),
|
||||
|
||||
# Frenos
|
||||
('Frenos', 'Frenos (pastillas, discos)', 'pass_fail', 2, True),
|
||||
('Frenos', 'Líquido de freno', 'pass_fail', 1, False),
|
||||
('Frenos', 'Porcentaje de humedad', 'numeric', 1, False),
|
||||
|
||||
# Motor
|
||||
('Motor', 'Nivel de aceite', 'pass_fail', 1, True),
|
||||
('Motor', 'Fugas de aceite', 'pass_fail', 2, True),
|
||||
('Motor', 'Filtro de aceite', 'status', 1, True),
|
||||
('Motor', 'Fugas de refrigerante', 'pass_fail', 2, True),
|
||||
|
||||
# Neumáticos
|
||||
('Neumáticos', 'Presión neumáticos', 'pass_fail', 1, False),
|
||||
('Neumáticos', 'Banda de rodadura', 'good_bad', 1, True),
|
||||
|
||||
# Suspensión
|
||||
('Suspensión', 'Amortiguadores', 'pass_fail', 2, True),
|
||||
('Suspensión', 'Cojinetes de ruedas', 'pass_fail', 1, False),
|
||||
|
||||
# Varios
|
||||
('Exterior', 'Estado carrocería', 'good_bad', 1, True),
|
||||
('Exterior', 'Escobillas limpiaparabrisas', 'pass_fail', 1, True),
|
||||
('Interior', 'Aire acondicionado', 'pass_fail', 1, False),
|
||||
('Pruebas', 'Prueba dinámica del vehículo', 'pass_fail', 2, False),
|
||||
]
|
||||
|
||||
max_score = 0
|
||||
for idx, (section, text, qtype, points, photos) in enumerate(questions_data):
|
||||
question = Question(
|
||||
checklist_id=checklist.id,
|
||||
section=section,
|
||||
text=text,
|
||||
type=qtype,
|
||||
points=points,
|
||||
order=idx + 1,
|
||||
allow_photos=photos,
|
||||
max_photos=3
|
||||
)
|
||||
db.add(question)
|
||||
max_score += points
|
||||
|
||||
checklist.max_score = max_score
|
||||
db.commit()
|
||||
|
||||
print(f'✅ Checklist creado con {len(questions_data)} preguntas')
|
||||
print(f'✅ Puntuación máxima: {max_score}')
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "✅ Inicialización completada!"
|
||||
echo ""
|
||||
echo "🌐 Accede a la aplicación:"
|
||||
echo " Frontend: http://localhost:5173"
|
||||
echo " API Docs: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo "👥 Usuarios de prueba:"
|
||||
echo " Admin: admin / admin123"
|
||||
echo " Mecánico: mecanico1 / mecanico123"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user