From 7fb2e40a1ef186ba0dbb0d0b9c87228be5dded0f Mon Sep 17 00:00:00 2001 From: ronalds Date: Thu, 27 Nov 2025 01:58:08 -0300 Subject: [PATCH] v1.0.65 Backend / v1.0.59 Frontend - Fix client_name + Mejoras en carga de fotos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (1.0.65): - Fix: Todas las referencias client_name cambiadas a order_number - Actualizado webhook n8n: "cliente" → "pedido" - Actualizado contexto IA: "Cliente" → "Nº Pedido" - PDF ahora muestra "Nº de Pedido" en lugar de "Cliente" Frontend (1.0.59): - 📸 NUEVO: Vista previa de fotos cargadas (grid 3 columnas con thumbnails) - 📸 NUEVO: Botón "✕" para eliminar fotos individuales - 📸 NUEVO: Botón manual "🤖 Analizar con IA" (no auto-análisis) - 📸 MEJORA: Permite cargar múltiples fotos respetando max_photos - 📸 MEJORA: Input file solo required si no hay fotos cargadas - 📸 MEJORA: Muestra contador "X foto(s) cargada(s)" - 🔧 Fix: Ya no analiza automáticamente al subir (espera click en botón) - 🔧 Fix: Permite re-cargar fotos eliminando las anteriores - 🔧 Fix: Previene exceder max_photos mostrando alerta UX Improvements: - Usuario sube 1-3 fotos y las ve en preview - Puede eliminar individualmente con hover + click en ✕ - Click en "Analizar con IA" procesa todas las fotos juntas - Análisis secuencial con summary multi-imagen Nota: No requiere migración (ya ejecutada en v1.0.64) --- backend/app/main.py | 10 ++--- frontend/package.json | 2 +- frontend/src/App.jsx | 102 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index c697991..18c667a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -57,7 +57,7 @@ def send_answer_notification(answer, question, mechanic, db): "vehiculo_placa": inspection.vehicle_plate, "vehiculo_marca": inspection.vehicle_brand, "vehiculo_modelo": inspection.vehicle_model, - "cliente": inspection.client_name, + "pedido": inspection.order_number, "or_number": inspection.or_number }, "mecanico": { @@ -153,7 +153,7 @@ def send_completed_inspection_to_n8n(inspection, db): "modelo": inspection.vehicle_model, "kilometraje": inspection.vehicle_km }, - "cliente": inspection.client_name, + "pedido": inspection.order_number, "mecanico": { "id": mechanic.id if mechanic else None, "nombre": mechanic.full_name if mechanic else None, @@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.64" +BACKEND_VERSION = "1.0.65" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -1318,7 +1318,7 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str: inspection_data = [ [Paragraph("👤 INFORMACIÓN DEL CLIENTE", info_style)], [Table([ - [Paragraph("Cliente:", small_style), Paragraph(f"{inspection.client_name or 'N/A'}", info_style)], + [Paragraph("Nº Pedido:", small_style), Paragraph(f"{inspection.order_number or 'N/A'}", info_style)], [Paragraph("OR N°:", small_style), Paragraph(f"{inspection.or_number or 'N/A'}", info_style)], [Paragraph("Mecánico:", small_style), Paragraph(f"{mechanic.full_name if mechanic else 'N/A'}", info_style)], [Paragraph("Cód. Operario:", small_style), Paragraph(f"{inspection.mechanic_employee_code or 'N/A'}", info_style)], @@ -2175,7 +2175,7 @@ INFORMACIÓN DEL VEHÍCULO INSPECCIONADO: - Modelo: {inspection.vehicle_model} - Placa: {inspection.vehicle_plate} - Kilometraje: {inspection.vehicle_km} km -- Cliente: {inspection.client_name} +- Nº Pedido: {inspection.order_number} - OR/Orden: {inspection.or_number} """ else: diff --git a/frontend/package.json b/frontend/package.json index 26eb76f..fe10d24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "checklist-frontend", "private": true, - "version": "1.0.58", + "version": "1.0.59", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 635ad71..1a1327a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3615,25 +3615,46 @@ function InspectionModal({ checklist, user, onClose, onComplete }) { const question = questions.find(q => q.id === questionId) let filesArray = Array.from(files) + // Get existing photos + const existingPhotos = answers[questionId]?.photos || [] + + // Combine existing and new photos + const allPhotos = [...existingPhotos, ...filesArray] + // Validar límite de fotos - if (question.max_photos && filesArray.length > question.max_photos) { + if (question.max_photos && allPhotos.length > question.max_photos) { alert(`⚠️ Solo puedes subir hasta ${question.max_photos} foto${question.max_photos > 1 ? 's' : ''} para esta pregunta`) - filesArray = filesArray.slice(0, question.max_photos) + return } - // Update photos immediately + // Update photos immediately (do NOT auto-analyze) setAnswers(prev => ({ ...prev, [questionId]: { ...(prev[questionId] || { value: '', observations: '', photos: [] }), - photos: filesArray + photos: allPhotos } })) - - // If AI mode is assisted or full, analyze the photos - if ((checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && filesArray.length > 0) { - await analyzePhotosWithAI(questionId, filesArray) + } + + const handleRemovePhoto = (questionId, photoIndex) => { + setAnswers(prev => ({ + ...prev, + [questionId]: { + ...(prev[questionId] || { value: '', observations: '', photos: [] }), + photos: prev[questionId].photos.filter((_, index) => index !== photoIndex) + } + })) + } + + const handleAnalyzePhotos = async (questionId) => { + const photos = answers[questionId]?.photos || [] + if (photos.length === 0) { + alert('Primero debes subir al menos una foto') + return } + + await analyzePhotosWithAI(questionId, photos) } const analyzePhotosWithAI = async (questionId, files) => { @@ -4197,10 +4218,11 @@ function InspectionModal({ checklist, user, onClose, onComplete }) { )} {(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && ( - 🤖 Análisis IA activado + 🤖 Análisis IA disponible )} + handlePhotoChange(currentQuestion.id, e.target.files)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" disabled={aiAnalyzing} - required + required={!answers[currentQuestion.id]?.photos?.length} /> + {/* Photo Previews */} + {answers[currentQuestion.id]?.photos?.length > 0 && ( +
+
+ {answers[currentQuestion.id].photos.length} foto(s) cargada(s): +
+
+ {answers[currentQuestion.id].photos.map((photo, index) => ( +
+ {`Foto + +
+ Foto {index + 1} +
+
+ ))} +
+ + {/* Analyze Button */} + {(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && ( + + )} +
+ )} + {aiAnalyzing && (
- Analizando imagen con IA... + Analizando {answers[currentQuestion.id]?.photos?.length || 0} imagen(es) con IA...
)} {answers[currentQuestion.id]?.photos?.length > 0 && !aiAnalyzing && (
- {answers[currentQuestion.id].photos.length} foto(s) seleccionada(s) {checklist.ai_mode === 'full' && answers[currentQuestion.id]?.value && ( - ✓ Analizada + ✓ Analizada )} {checklist.ai_mode === 'assisted' && answers[currentQuestion.id]?.observations.includes('[IA Sugiere') && ( - ✓ Sugerencia generada + ✓ Sugerencia generada )}
)}