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)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -2049,54 +2049,30 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
|
|
||||||
# ===== LÓGICA ESPECIAL PARA AI_ASSISTANT =====
|
# ===== LÓGICA ESPECIAL PARA AI_ASSISTANT =====
|
||||||
if is_ai_assistant and ans.chat_history:
|
if is_ai_assistant and ans.chat_history:
|
||||||
# Generar resumen estructurado del chat
|
# Mostrar resumen simple SIN generar con IA (para evitar lentitud y peso)
|
||||||
import asyncio
|
|
||||||
try:
|
try:
|
||||||
# Ejecutar función async de forma sincrónica
|
chat_data = ans.chat_history if isinstance(ans.chat_history, list) else json.loads(ans.chat_history)
|
||||||
loop = asyncio.new_event_loop()
|
total_messages = len(chat_data)
|
||||||
asyncio.set_event_loop(loop)
|
user_messages = sum(1 for m in chat_data if m.get('role') == 'user')
|
||||||
chat_summary = loop.run_until_complete(
|
assistant_messages = sum(1 for m in chat_data if m.get('role') == 'assistant')
|
||||||
generate_chat_summary(ans.chat_history, question.text)
|
|
||||||
)
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
# Renderizar informe narrativo
|
|
||||||
question_data.append([
|
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,
|
ParagraphStyle('chat_title', parent=answer_style, fontSize=11,
|
||||||
textColor=colors.HexColor('#2563eb'), fontName='Helvetica-Bold'))
|
textColor=colors.HexColor('#2563eb'), fontName='Helvetica-Bold'))
|
||||||
])
|
])
|
||||||
|
|
||||||
# Problema identificado
|
|
||||||
question_data.append([
|
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)
|
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:
|
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
|
# Fallback: mostrar que hubo conversación
|
||||||
question_data.append([
|
question_data.append([
|
||||||
Table([
|
Table([
|
||||||
@@ -2284,9 +2260,15 @@ def complete_inspection(
|
|||||||
inspection.status = "completed"
|
inspection.status = "completed"
|
||||||
inspection.completed_at = datetime.utcnow()
|
inspection.completed_at = datetime.utcnow()
|
||||||
|
|
||||||
# Generar PDF usando función reutilizable
|
# Generar PDF solo si el checklist lo tiene habilitado
|
||||||
pdf_url = generate_inspection_pdf(inspection_id, db)
|
if inspection.checklist.generate_pdf:
|
||||||
inspection.pdf_url = pdf_url
|
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.commit()
|
||||||
db.refresh(inspection)
|
db.refresh(inspection)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class Checklist(Base):
|
|||||||
scoring_enabled = Column(Boolean, default=True)
|
scoring_enabled = Column(Boolean, default=True)
|
||||||
max_score = Column(Integer, default=0)
|
max_score = Column(Integer, default=0)
|
||||||
logo_url = Column(String(500))
|
logo_url = Column(String(500))
|
||||||
|
generate_pdf = Column(Boolean, default=True) # Controla si se genera PDF al completar
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
created_by = Column(Integer, ForeignKey("users.id"))
|
created_by = Column(Integer, ForeignKey("users.id"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class ChecklistBase(BaseModel):
|
|||||||
ai_mode: str = "off"
|
ai_mode: str = "off"
|
||||||
scoring_enabled: bool = True
|
scoring_enabled: bool = True
|
||||||
logo_url: Optional[str] = None
|
logo_url: Optional[str] = None
|
||||||
|
generate_pdf: bool = True
|
||||||
|
|
||||||
class ChecklistCreate(ChecklistBase):
|
class ChecklistCreate(ChecklistBase):
|
||||||
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
|
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
|
||||||
@@ -81,12 +82,14 @@ class ChecklistUpdate(BaseModel):
|
|||||||
ai_mode: Optional[str] = None
|
ai_mode: Optional[str] = None
|
||||||
scoring_enabled: Optional[bool] = None
|
scoring_enabled: Optional[bool] = None
|
||||||
logo_url: Optional[str] = None
|
logo_url: Optional[str] = None
|
||||||
|
generate_pdf: Optional[bool] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
||||||
|
|
||||||
class Checklist(ChecklistBase):
|
class Checklist(ChecklistBase):
|
||||||
id: int
|
id: int
|
||||||
max_score: int
|
max_score: int
|
||||||
|
generate_pdf: bool
|
||||||
is_active: bool
|
is_active: bool
|
||||||
created_by: int
|
created_by: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.6",
|
"version": "1.3.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Service Worker para PWA con detección de actualizaciones
|
// Service Worker para PWA con detección de actualizaciones
|
||||||
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
|
// 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 = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html'
|
'/index.html'
|
||||||
|
|||||||
@@ -2302,7 +2302,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
ai_mode: 'off',
|
ai_mode: 'off',
|
||||||
scoring_enabled: true
|
scoring_enabled: true,
|
||||||
|
generate_pdf: true
|
||||||
})
|
})
|
||||||
const [editPermissionsData, setEditPermissionsData] = useState({
|
const [editPermissionsData, setEditPermissionsData] = useState({
|
||||||
mechanic_ids: []
|
mechanic_ids: []
|
||||||
@@ -2673,7 +2674,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
name: checklist.name,
|
name: checklist.name,
|
||||||
description: checklist.description || '',
|
description: checklist.description || '',
|
||||||
ai_mode: checklist.ai_mode || 'off',
|
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)
|
setShowEditChecklistModal(true)
|
||||||
}}
|
}}
|
||||||
@@ -3049,6 +3051,18 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<p className="text-sm text-blue-800">
|
<p className="text-sm text-blue-800">
|
||||||
ℹ️ Los cambios se aplicarán inmediatamente. Las inspecciones existentes no se verán afectadas.
|
ℹ️ 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={() => {
|
onClick={() => {
|
||||||
setShowEditChecklistModal(false)
|
setShowEditChecklistModal(false)
|
||||||
setSelectedChecklist(null)
|
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"
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
@@ -3264,6 +3278,9 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
|
|||||||
const [showAuditLog, setShowAuditLog] = useState(false)
|
const [showAuditLog, setShowAuditLog] = useState(false)
|
||||||
const [auditLogs, setAuditLogs] = useState([])
|
const [auditLogs, setAuditLogs] = useState([])
|
||||||
const [loadingAudit, setLoadingAudit] = useState(false)
|
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
|
// Función helper para convertir valores técnicos a etiquetas legibles
|
||||||
const getReadableAnswer = (answerValue, questionOptions) => {
|
const getReadableAnswer = (answerValue, questionOptions) => {
|
||||||
@@ -3698,6 +3715,37 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
|
|||||||
</div>
|
</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 */}
|
{/* Photos - NUEVO: miniaturas de media_files */}
|
||||||
{(answer.media_files && answer.media_files.length > 0) && (
|
{(answer.media_files && answer.media_files.length > 0) && (
|
||||||
<div className="flex gap-2 flex-wrap mt-2">
|
<div className="flex gap-2 flex-wrap mt-2">
|
||||||
@@ -4018,6 +4066,88 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</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"
|
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">
|
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
|
||||||
Ayutec v1.3.6
|
Ayutec v1.3.8
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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