Cambios realizados v1.2.11 (Backend) + v1.3.8 (Frontend):

Backend v1.2.11:
Nueva Funcionalidad - Control de Generación de PDF:
Campo nuevo: generate_pdf en modelo Checklist (Boolean, default: True)
Lógica modificada: Al completar inspección se verifica si el checklist tiene habilitada la generación de PDF
Comportamiento:
Si generate_pdf = True → Se genera y guarda el PDF automáticamente
Si generate_pdf = False → No se genera PDF, pdf_url queda en NULL
Logs informativos: Muestra en consola si el PDF se generó o se omitió
Frontend v1.3.8:
Interfaz para Control de PDF:
Checkbox nuevo en modal de edición de checklist: "Generar PDF automáticamente al completar inspección"
Estado por defecto: Activado (mantiene comportamiento actual)
Persistencia: El valor se guarda en la base de datos al editar checklist
Dónde está:
Admin → Checklists → Click en "✏️ Editar" de cualquier checklist
Debajo del checkbox de "Habilitar sistema de puntuación"
This commit is contained in:
2025-12-08 09:44:33 -03:00
parent 7fd37d0992
commit 0c0812efe9
8 changed files with 175 additions and 46 deletions

View File

@@ -278,7 +278,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict:
}
BACKEND_VERSION = "1.2.9"
BACKEND_VERSION = "1.2.11"
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration
@@ -2049,54 +2049,30 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
# ===== LÓGICA ESPECIAL PARA AI_ASSISTANT =====
if is_ai_assistant and ans.chat_history:
# Generar resumen estructurado del chat
import asyncio
# Mostrar resumen simple SIN generar con IA (para evitar lentitud y peso)
try:
# Ejecutar función async de forma sincrónica
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
chat_summary = loop.run_until_complete(
generate_chat_summary(ans.chat_history, question.text)
)
loop.close()
chat_data = ans.chat_history if isinstance(ans.chat_history, list) else json.loads(ans.chat_history)
total_messages = len(chat_data)
user_messages = sum(1 for m in chat_data if m.get('role') == 'user')
assistant_messages = sum(1 for m in chat_data if m.get('role') == 'assistant')
# Renderizar informe narrativo
question_data.append([
Paragraph(f"<b>💬 INFORME DE DIAGNÓSTICO ASISTIDO</b>",
Paragraph(f"<b>💬 DIAGNÓSTICO ASISTIDO POR IA</b>",
ParagraphStyle('chat_title', parent=answer_style, fontSize=11,
textColor=colors.HexColor('#2563eb'), fontName='Helvetica-Bold'))
])
# Problema identificado
question_data.append([
Paragraph(f"<b>🔍 Problema Identificado:</b><br/>{chat_summary.get('problema_identificado', 'N/A')}",
Paragraph(f"<b>📊 Resumen de Conversación:</b><br/>"
f"• Total de mensajes: {total_messages}<br/>"
f"• Consultas del mecánico: {user_messages}<br/>"
f"• Respuestas del asistente: {assistant_messages}<br/><br/>"
f"<i>Nota: El historial completo está disponible en el sistema para administradores.</i>",
comment_style)
])
# Hallazgos
if chat_summary.get('hallazgos') and len(chat_summary['hallazgos']) > 0:
hallazgos_text = "<br/>".join([f"{h}" for h in chat_summary['hallazgos']])
question_data.append([
Paragraph(f"<b>📋 Hallazgos:</b><br/>{hallazgos_text}", comment_style)
])
# Diagnóstico
question_data.append([
Paragraph(f"<b>🔧 Diagnóstico:</b><br/>{chat_summary.get('diagnostico', 'N/A')}",
comment_style)
])
# Recomendaciones
if chat_summary.get('recomendaciones') and len(chat_summary['recomendaciones']) > 0:
recomendaciones_text = "<br/>".join([f"{r}" for r in chat_summary['recomendaciones']])
question_data.append([
Paragraph(f"<b>✅ Recomendaciones:</b><br/>{recomendaciones_text}",
ParagraphStyle('recommendations', parent=comment_style,
textColor=colors.HexColor('#16a34a')))
])
except Exception as e:
print(f"❌ Error generando resumen de chat en PDF: {e}")
print(f"❌ Error procesando chat en PDF: {e}")
# Fallback: mostrar que hubo conversación
question_data.append([
Table([
@@ -2284,9 +2260,15 @@ def complete_inspection(
inspection.status = "completed"
inspection.completed_at = datetime.utcnow()
# Generar PDF usando función reutilizable
pdf_url = generate_inspection_pdf(inspection_id, db)
inspection.pdf_url = pdf_url
# Generar PDF solo si el checklist lo tiene habilitado
if inspection.checklist.generate_pdf:
pdf_url = generate_inspection_pdf(inspection_id, db)
inspection.pdf_url = pdf_url
print(f"✅ PDF generado para inspección #{inspection_id}")
else:
inspection.pdf_url = None
print(f"⏭️ PDF NO generado (deshabilitado en checklist) para inspección #{inspection_id}")
db.commit()
db.refresh(inspection)

View File

@@ -47,6 +47,7 @@ class Checklist(Base):
scoring_enabled = Column(Boolean, default=True)
max_score = Column(Integer, default=0)
logo_url = Column(String(500))
generate_pdf = Column(Boolean, default=True) # Controla si se genera PDF al completar
is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -71,6 +71,7 @@ class ChecklistBase(BaseModel):
ai_mode: str = "off"
scoring_enabled: bool = True
logo_url: Optional[str] = None
generate_pdf: bool = True
class ChecklistCreate(ChecklistBase):
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
@@ -81,12 +82,14 @@ class ChecklistUpdate(BaseModel):
ai_mode: Optional[str] = None
scoring_enabled: Optional[bool] = None
logo_url: Optional[str] = None
generate_pdf: Optional[bool] = 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
generate_pdf: bool
is_active: bool
created_by: int
created_at: datetime

View File

@@ -1,7 +1,7 @@
{
"name": "checklist-frontend",
"private": true,
"version": "1.3.6",
"version": "1.3.8",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
// Service Worker para PWA con detección de actualizaciones
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
const CACHE_NAME = 'ayutec-v1.3.6';
const CACHE_NAME = 'ayutec-v1.3.8';
const urlsToCache = [
'/',
'/index.html'

View File

@@ -2302,7 +2302,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
name: '',
description: '',
ai_mode: 'off',
scoring_enabled: true
scoring_enabled: true,
generate_pdf: true
})
const [editPermissionsData, setEditPermissionsData] = useState({
mechanic_ids: []
@@ -2673,7 +2674,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
name: checklist.name,
description: checklist.description || '',
ai_mode: checklist.ai_mode || 'off',
scoring_enabled: checklist.scoring_enabled ?? true
scoring_enabled: checklist.scoring_enabled ?? true,
generate_pdf: checklist.generate_pdf ?? true
})
setShowEditChecklistModal(true)
}}
@@ -3049,6 +3051,18 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={editChecklistData.generate_pdf !== false}
onChange={(e) => setEditChecklistData({ ...editChecklistData, generate_pdf: e.target.checked })}
className="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
/>
<label className="ml-2 text-sm text-gray-700">
Generar PDF automáticamente al completar inspección
</label>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
Los cambios se aplicarán inmediatamente. Las inspecciones existentes no se verán afectadas.
@@ -3061,7 +3075,7 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
onClick={() => {
setShowEditChecklistModal(false)
setSelectedChecklist(null)
setEditChecklistData({ name: '', description: '', ai_mode: 'off', scoring_enabled: true })
setEditChecklistData({ name: '', description: '', ai_mode: 'off', scoring_enabled: true, generate_pdf: true })
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
disabled={updating}
@@ -3264,6 +3278,9 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
const [showAuditLog, setShowAuditLog] = useState(false)
const [auditLogs, setAuditLogs] = useState([])
const [loadingAudit, setLoadingAudit] = useState(false)
const [showChatHistory, setShowChatHistory] = useState(false)
const [selectedChatHistory, setSelectedChatHistory] = useState(null)
const [selectedQuestionText, setSelectedQuestionText] = useState('')
// Función helper para convertir valores técnicos a etiquetas legibles
const getReadableAnswer = (answerValue, questionOptions) => {
@@ -3698,6 +3715,37 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
</div>
)}
{/* Chat History - SOLO PARA ADMINS */}
{user?.role === 'admin' && answer.chat_history && (
<div className="bg-blue-50 border border-blue-200 rounded p-3 mt-2">
<div className="flex items-center justify-between">
<span className="text-xs text-blue-800 font-semibold">💬 Historial de Chat IA</span>
<button
onClick={() => {
setSelectedChatHistory(answer.chat_history)
setSelectedQuestionText(question.text)
setShowChatHistory(true)
}}
className="px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-xs"
>
Ver Conversación
</button>
</div>
<div className="text-xs text-blue-700 mt-1">
{(() => {
try {
const chatData = typeof answer.chat_history === 'string'
? JSON.parse(answer.chat_history)
: answer.chat_history
return `${chatData.length} mensajes`
} catch {
return 'Historial disponible'
}
})()}
</div>
</div>
)}
{/* Photos - NUEVO: miniaturas de media_files */}
{(answer.media_files && answer.media_files.length > 0) && (
<div className="flex gap-2 flex-wrap mt-2">
@@ -4018,6 +4066,88 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
</div>
</div>
)}
{/* Modal de Historial de Chat */}
{showChatHistory && selectedChatHistory && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60] p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4">
<div className="flex justify-between items-start">
<div>
<h3 className="text-xl font-bold">💬 Historial de Chat IA</h3>
<p className="text-sm opacity-90 mt-1">{selectedQuestionText}</p>
</div>
<button
onClick={() => setShowChatHistory(false)}
className="text-white hover:bg-white hover:bg-opacity-20 rounded-lg p-2 transition"
>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{(() => {
try {
const chatData = typeof selectedChatHistory === 'string'
? JSON.parse(selectedChatHistory)
: selectedChatHistory
return chatData.map((msg, idx) => (
<div
key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg p-3 ${
msg.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-800 border border-gray-200'
}`}
>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-semibold opacity-75">
{msg.role === 'user' ? '👤 Mecánico' : '🤖 Asistente IA'}
</span>
<span className="text-xs opacity-60">
{new Date(msg.timestamp).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
<div className="text-sm whitespace-pre-wrap break-words">
{msg.content}
</div>
{msg.confidence && (
<div className="text-xs mt-2 opacity-75">
Confianza: {Math.round(msg.confidence * 100)}%
</div>
)}
</div>
</div>
))
} catch (error) {
return (
<div className="text-center text-red-600">
Error al cargar el historial de chat
</div>
)
}
})()}
</div>
<div className="border-t p-4 bg-gray-50">
<button
onClick={() => setShowChatHistory(false)}
className="w-full px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition"
>
Cerrar
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -153,7 +153,7 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
className="w-10 h-10 object-contain bg-white rounded p-1"
/>
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
Ayutec v1.3.6
Ayutec v1.3.8
</p>
</a>
</div>

View File

@@ -0,0 +1,13 @@
-- Agregar campo generate_pdf a la tabla checklists
-- Este campo controla si se genera PDF automáticamente al completar una inspección
ALTER TABLE checklists
ADD COLUMN IF NOT EXISTS generate_pdf BOOLEAN DEFAULT TRUE;
-- Actualizar checklists existentes para que generen PDF por defecto
UPDATE checklists
SET generate_pdf = TRUE
WHERE generate_pdf IS NULL;
-- Comentario para documentación
COMMENT ON COLUMN checklists.generate_pdf IS 'Controla si se genera PDF automáticamente al completar inspección';