- Implementado soft delete para preguntas
- Nuevas columnas: is_deleted (boolean), updated_at (timestamp)
- Migración SQL: add_soft_delete_to_questions.sql
- Endpoint DELETE marca preguntas como eliminadas en lugar de borrarlas
- GET /api/checklists/{id} filtra preguntas eliminadas (is_deleted=false)
- Validación de subpreguntas activas antes de eliminar
- Índices agregados para optimizar queries
- Mantiene integridad de respuestas históricas y PDFs generados
- Permite limpiar checklists sin afectar inspecciones completadas
360 lines
9.0 KiB
Python
360 lines
9.0 KiB
Python
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
|
|
employee_code: Optional[str] = None # Nro Operario - código de otro sistema
|
|
role: str = "mechanic"
|
|
|
|
class UserCreate(UserBase):
|
|
password: str
|
|
|
|
class UserUpdate(BaseModel):
|
|
username: Optional[str] = None
|
|
email: Optional[EmailStr] = None
|
|
full_name: Optional[str] = None
|
|
employee_code: Optional[str] = None
|
|
role: Optional[str] = None
|
|
|
|
class UserPasswordUpdate(BaseModel):
|
|
current_password: str
|
|
new_password: str
|
|
|
|
class AdminPasswordUpdate(BaseModel):
|
|
new_password: str
|
|
|
|
class UserLogin(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
class User(UserBase):
|
|
id: int
|
|
employee_code: Optional[str] = None
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
token_type: str
|
|
user: User
|
|
|
|
|
|
# API Token Schemas
|
|
class APITokenCreate(BaseModel):
|
|
description: Optional[str] = None
|
|
|
|
class APIToken(BaseModel):
|
|
id: int
|
|
description: Optional[str] = None
|
|
is_active: bool
|
|
last_used_at: Optional[datetime] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class APITokenWithValue(APIToken):
|
|
token: str
|
|
|
|
|
|
# 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):
|
|
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
|
|
|
|
class ChecklistUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
ai_mode: Optional[str] = None
|
|
scoring_enabled: Optional[bool] = None
|
|
logo_url: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
|
|
|
class Checklist(ChecklistBase):
|
|
id: int
|
|
max_score: int
|
|
is_active: bool
|
|
created_by: int
|
|
created_at: datetime
|
|
allowed_mechanics: Optional[List[int]] = [] # IDs de mecánicos permitidos
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Question Schemas
|
|
# Tipos de preguntas soportados:
|
|
# - boolean: Dos opciones personalizables (ej: Sí/No, Pasa/Falla)
|
|
# - single_choice: Selección única con N opciones
|
|
# - multiple_choice: Selección múltiple
|
|
# - scale: Escala numérica (1-5, 1-10, etc.)
|
|
# - text: Texto libre
|
|
# - number: Valor numérico
|
|
# - date: Fecha
|
|
# - time: Hora
|
|
|
|
class QuestionBase(BaseModel):
|
|
section: Optional[str] = None
|
|
text: str
|
|
type: str # boolean, single_choice, multiple_choice, scale, text, number, date, time
|
|
points: int = 1
|
|
options: Optional[dict] = None # Configuración flexible según tipo
|
|
# Estructura de options:
|
|
# Boolean: {"type": "boolean", "choices": [{"value": "yes", "label": "Sí", "points": 1, "status": "ok"}, ...]}
|
|
# Single/Multiple Choice: {"type": "single_choice", "choices": [{"value": "opt1", "label": "Opción 1", "points": 2}, ...]}
|
|
# Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}
|
|
# Text: {"type": "text", "multiline": true, "max_length": 500}
|
|
order: int = 0
|
|
allow_photos: bool = True
|
|
max_photos: int = 3
|
|
requires_comment_on_fail: bool = False
|
|
send_notification: bool = False
|
|
parent_question_id: Optional[int] = None
|
|
show_if_answer: Optional[str] = None
|
|
ai_prompt: Optional[str] = None
|
|
is_deleted: bool = False
|
|
|
|
class QuestionCreate(QuestionBase):
|
|
checklist_id: int
|
|
|
|
class QuestionUpdate(QuestionBase):
|
|
pass
|
|
|
|
class Question(QuestionBase):
|
|
id: int
|
|
checklist_id: int
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Question Audit Schemas
|
|
class QuestionAuditLog(BaseModel):
|
|
id: int
|
|
question_id: int
|
|
checklist_id: int
|
|
user_id: int
|
|
action: str
|
|
field_name: Optional[str] = None
|
|
old_value: Optional[str] = None
|
|
new_value: Optional[str] = None
|
|
comment: Optional[str] = None
|
|
created_at: datetime
|
|
user: Optional['User'] = None
|
|
|
|
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
|
|
order_number: Optional[str] = None # Nº de Pedido
|
|
mechanic_employee_code: 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
|
|
mechanic_employee_code: Optional[str] = None
|
|
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: Optional[str] = None # Opcional para permitir guardar solo análisis IA
|
|
status: str = "ok"
|
|
comment: Optional[str] = None
|
|
is_flagged: bool = False
|
|
|
|
class AnswerCreate(AnswerBase):
|
|
inspection_id: int
|
|
question_id: int
|
|
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
|
|
|
|
class AnswerUpdate(AnswerBase):
|
|
pass
|
|
|
|
class Answer(AnswerBase):
|
|
id: int
|
|
inspection_id: int
|
|
question_id: int
|
|
points_earned: int
|
|
ai_analysis: Optional[list] = None # Lista de análisis de IA
|
|
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 AnswerWithMedia(Answer):
|
|
media_files: List[MediaFile] = []
|
|
question: Question
|
|
|
|
class InspectionDetail(Inspection):
|
|
checklist: ChecklistWithQuestions
|
|
mechanic: User
|
|
answers: List[AnswerWithMedia] = []
|
|
|
|
|
|
# AI Configuration Schemas
|
|
class AIConfigurationBase(BaseModel):
|
|
provider: str # openai, gemini
|
|
api_key: str
|
|
model_name: Optional[str] = None
|
|
|
|
class AIConfigurationCreate(AIConfigurationBase):
|
|
pass
|
|
|
|
class AIConfigurationUpdate(BaseModel):
|
|
provider: Optional[str] = None
|
|
api_key: Optional[str] = None
|
|
model_name: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
class AIConfiguration(AIConfigurationBase):
|
|
id: int
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class AIModelInfo(BaseModel):
|
|
id: str
|
|
name: str
|
|
provider: str
|
|
description: Optional[str] = None
|
|
|
|
# Reports Schemas
|
|
class InspectionStats(BaseModel):
|
|
total_inspections: int
|
|
completed_inspections: int
|
|
pending_inspections: int
|
|
completion_rate: float
|
|
avg_score: float
|
|
total_flagged_items: int
|
|
|
|
class MechanicRanking(BaseModel):
|
|
mechanic_id: int
|
|
mechanic_name: str
|
|
total_inspections: int
|
|
avg_score: float
|
|
completion_rate: float
|
|
|
|
class ChecklistStats(BaseModel):
|
|
checklist_id: int
|
|
checklist_name: str
|
|
total_inspections: int
|
|
avg_score: float
|
|
|
|
class DashboardData(BaseModel):
|
|
stats: InspectionStats
|
|
mechanic_ranking: List[MechanicRanking]
|
|
checklist_stats: List[ChecklistStats]
|
|
inspections_by_date: dict
|
|
pass_fail_ratio: dict
|
|
|
|
class InspectionListItem(BaseModel):
|
|
id: int
|
|
vehicle_plate: str
|
|
checklist_name: str
|
|
mechanic_name: str
|
|
status: str
|
|
score: Optional[int]
|
|
max_score: Optional[int]
|
|
flagged_items: int
|
|
started_at: Optional[datetime]
|
|
completed_at: Optional[datetime]
|
|
|
|
|
|
# Audit Log Schemas
|
|
class AuditLogBase(BaseModel):
|
|
action: str
|
|
entity_type: str
|
|
field_name: Optional[str] = None
|
|
old_value: Optional[str] = None
|
|
new_value: Optional[str] = None
|
|
comment: Optional[str] = None
|
|
|
|
class AuditLog(AuditLogBase):
|
|
id: int
|
|
inspection_id: int
|
|
answer_id: Optional[int] = None
|
|
user_id: int
|
|
user_name: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class AnswerEdit(BaseModel):
|
|
answer_value: Optional[str] = None
|
|
status: Optional[str] = None
|
|
comment: Optional[str] = None
|
|
is_flagged: Optional[bool] = None
|
|
edit_comment: Optional[str] = None # Comentario del admin sobre por qué editó
|