v1.0.65 Backend / v1.0.59 Frontend - Fix client_name + Mejoras en carga de fotos
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)
This commit is contained in:
@@ -57,7 +57,7 @@ def send_answer_notification(answer, question, mechanic, db):
|
|||||||
"vehiculo_placa": inspection.vehicle_plate,
|
"vehiculo_placa": inspection.vehicle_plate,
|
||||||
"vehiculo_marca": inspection.vehicle_brand,
|
"vehiculo_marca": inspection.vehicle_brand,
|
||||||
"vehiculo_modelo": inspection.vehicle_model,
|
"vehiculo_modelo": inspection.vehicle_model,
|
||||||
"cliente": inspection.client_name,
|
"pedido": inspection.order_number,
|
||||||
"or_number": inspection.or_number
|
"or_number": inspection.or_number
|
||||||
},
|
},
|
||||||
"mecanico": {
|
"mecanico": {
|
||||||
@@ -153,7 +153,7 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
"modelo": inspection.vehicle_model,
|
"modelo": inspection.vehicle_model,
|
||||||
"kilometraje": inspection.vehicle_km
|
"kilometraje": inspection.vehicle_km
|
||||||
},
|
},
|
||||||
"cliente": inspection.client_name,
|
"pedido": inspection.order_number,
|
||||||
"mecanico": {
|
"mecanico": {
|
||||||
"id": mechanic.id if mechanic else None,
|
"id": mechanic.id if mechanic else None,
|
||||||
"nombre": mechanic.full_name 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
|
# 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)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -1318,7 +1318,7 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
inspection_data = [
|
inspection_data = [
|
||||||
[Paragraph("<b>👤 INFORMACIÓN DEL CLIENTE</b>", info_style)],
|
[Paragraph("<b>👤 INFORMACIÓN DEL CLIENTE</b>", info_style)],
|
||||||
[Table([
|
[Table([
|
||||||
[Paragraph("<b>Cliente:</b>", small_style), Paragraph(f"{inspection.client_name or 'N/A'}", info_style)],
|
[Paragraph("<b>Nº Pedido:</b>", small_style), Paragraph(f"{inspection.order_number or 'N/A'}", info_style)],
|
||||||
[Paragraph("<b>OR N°:</b>", small_style), Paragraph(f"{inspection.or_number or 'N/A'}", info_style)],
|
[Paragraph("<b>OR N°:</b>", small_style), Paragraph(f"{inspection.or_number or 'N/A'}", info_style)],
|
||||||
[Paragraph("<b>Mecánico:</b>", small_style), Paragraph(f"{mechanic.full_name if mechanic else 'N/A'}", info_style)],
|
[Paragraph("<b>Mecánico:</b>", small_style), Paragraph(f"{mechanic.full_name if mechanic else 'N/A'}", info_style)],
|
||||||
[Paragraph("<b>Cód. Operario:</b>", small_style), Paragraph(f"{inspection.mechanic_employee_code or 'N/A'}", info_style)],
|
[Paragraph("<b>Cód. Operario:</b>", 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}
|
- Modelo: {inspection.vehicle_model}
|
||||||
- Placa: {inspection.vehicle_plate}
|
- Placa: {inspection.vehicle_plate}
|
||||||
- Kilometraje: {inspection.vehicle_km} km
|
- Kilometraje: {inspection.vehicle_km} km
|
||||||
- Cliente: {inspection.client_name}
|
- Nº Pedido: {inspection.order_number}
|
||||||
- OR/Orden: {inspection.or_number}
|
- OR/Orden: {inspection.or_number}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.58",
|
"version": "1.0.59",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -3615,25 +3615,46 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
const question = questions.find(q => q.id === questionId)
|
const question = questions.find(q => q.id === questionId)
|
||||||
let filesArray = Array.from(files)
|
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
|
// 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`)
|
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 => ({
|
setAnswers(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[questionId]: {
|
[questionId]: {
|
||||||
...(prev[questionId] || { value: '', observations: '', photos: [] }),
|
...(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) {
|
const handleRemovePhoto = (questionId, photoIndex) => {
|
||||||
await analyzePhotosWithAI(questionId, filesArray)
|
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) => {
|
const analyzePhotosWithAI = async (questionId, files) => {
|
||||||
@@ -4197,10 +4218,11 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
)}
|
)}
|
||||||
{(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && (
|
{(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && (
|
||||||
<span className="ml-2 text-xs text-blue-600">
|
<span className="ml-2 text-xs text-blue-600">
|
||||||
🤖 Análisis IA activado
|
🤖 Análisis IA disponible
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -4208,26 +4230,78 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
onChange={(e) => handlePhotoChange(currentQuestion.id, e.target.files)}
|
onChange={(e) => 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"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
disabled={aiAnalyzing}
|
disabled={aiAnalyzing}
|
||||||
required
|
required={!answers[currentQuestion.id]?.photos?.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Photo Previews */}
|
||||||
|
{answers[currentQuestion.id]?.photos?.length > 0 && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div className="text-sm font-medium text-gray-700">
|
||||||
|
{answers[currentQuestion.id].photos.length} foto(s) cargada(s):
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{answers[currentQuestion.id].photos.map((photo, index) => (
|
||||||
|
<div key={index} className="relative group">
|
||||||
|
<img
|
||||||
|
src={URL.createObjectURL(photo)}
|
||||||
|
alt={`Foto ${index + 1}`}
|
||||||
|
className="w-full h-24 object-cover rounded-lg border border-gray-300"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemovePhoto(currentQuestion.id, index)}
|
||||||
|
className="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Eliminar foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<div className="text-xs text-center text-gray-600 mt-1">
|
||||||
|
Foto {index + 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analyze Button */}
|
||||||
|
{(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleAnalyzePhotos(currentQuestion.id)}
|
||||||
|
disabled={aiAnalyzing}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{aiAnalyzing ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
|
||||||
|
<span>Analizando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>🤖</span>
|
||||||
|
<span>Analizar con IA</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{aiAnalyzing && (
|
{aiAnalyzing && (
|
||||||
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
<div className="flex items-center gap-2 text-blue-700">
|
<div className="flex items-center gap-2 text-blue-700">
|
||||||
<div className="animate-spin h-4 w-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
<div className="animate-spin h-4 w-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
||||||
<span className="text-sm font-medium">Analizando imagen con IA...</span>
|
<span className="text-sm font-medium">Analizando {answers[currentQuestion.id]?.photos?.length || 0} imagen(es) con IA...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{answers[currentQuestion.id]?.photos?.length > 0 && !aiAnalyzing && (
|
{answers[currentQuestion.id]?.photos?.length > 0 && !aiAnalyzing && (
|
||||||
<div className="text-sm text-gray-600 mt-1">
|
<div className="text-sm text-gray-600 mt-1">
|
||||||
{answers[currentQuestion.id].photos.length} foto(s) seleccionada(s)
|
|
||||||
{checklist.ai_mode === 'full' && answers[currentQuestion.id]?.value && (
|
{checklist.ai_mode === 'full' && answers[currentQuestion.id]?.value && (
|
||||||
<span className="ml-2 text-green-600">✓ Analizada</span>
|
<span className="text-green-600">✓ Analizada</span>
|
||||||
)}
|
)}
|
||||||
{checklist.ai_mode === 'assisted' && answers[currentQuestion.id]?.observations.includes('[IA Sugiere') && (
|
{checklist.ai_mode === 'assisted' && answers[currentQuestion.id]?.observations.includes('[IA Sugiere') && (
|
||||||
<span className="ml-2 text-blue-600">✓ Sugerencia generada</span>
|
<span className="text-blue-600">✓ Sugerencia generada</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user