Backend (1.0.66): - 🐛 Fix: answer_value ahora es Optional en AnswerBase schema - Permite guardar respuestas con solo análisis IA y fotos - Permite guardar observaciones sin answer_value - Ya no rechaza con 422 cuando answer_value es null/vacío Frontend (1.0.60): - 🐛 Fix: saveAnswer ahora permite guardar si hay: * Valor de respuesta, O * Observaciones de IA, O * Fotos cargadas - Mejorada lógica de determinación de status - Solo calcula status si hay answer.value - Permite guardar análisis IA antes de seleccionar respuesta Flujo mejorado: 1. Usuario sube fotos 2. Click "Analizar con IA" → genera observaciones 3. Puede avanzar sin seleccionar respuesta (guardará solo observaciones) 4. O puede seleccionar respuesta después → actualiza el record Causa del error 422: - answer_value era required en schema - Al analizar fotos sin seleccionar respuesta se enviaba answer_value="" - Backend rechazaba con 422 Unprocessable Entity - Ahora answer_value es opcional y acepta null/vacío
358 lines
8.9 KiB
Python
358 lines
8.9 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
|
|
|
|
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
|
|
|
|
|
|
# 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[dict] = None # Análisis de IA completo
|
|
|
|
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 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ó
|