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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "checklist-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
13
migrations/add_generate_pdf_to_checklists.sql
Normal file
13
migrations/add_generate_pdf_to_checklists.sql
Normal 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';
|
||||
Reference in New Issue
Block a user