diff --git a/backend/app/main.py b/backend/app/main.py
index d132cb7..fe90e8d 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -209,7 +209,7 @@ def send_completed_inspection_to_n8n(inspection, db):
# No lanzamos excepción para no interrumpir el flujo normal
-BACKEND_VERSION = "1.0.93"
+BACKEND_VERSION = "1.0.94"
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration
diff --git a/backend/app/models.py b/backend/app/models.py
index 7d07411..6138ae0 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -70,7 +70,8 @@ class Question(Base):
points = Column(Integer, default=1)
options = Column(JSON) # Configuración flexible según tipo de pregunta
order = Column(Integer, default=0)
- allow_photos = Column(Boolean, default=True)
+ allow_photos = Column(Boolean, default=True) # DEPRECATED: usar photo_requirement
+ photo_requirement = Column(String(20), default='optional') # none, optional, required
max_photos = Column(Integer, default=3)
requires_comment_on_fail = Column(Boolean, default=False)
send_notification = Column(Boolean, default=False)
diff --git a/frontend/package.json b/frontend/package.json
index 72f3d3a..e1f5e91 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "checklist-frontend",
"private": true,
- "version": "1.0.99",
+ "version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js
index a25bd54..e1fcd1e 100644
--- a/frontend/public/service-worker.js
+++ b/frontend/public/service-worker.js
@@ -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.0.99';
+const CACHE_NAME = 'ayutec-v1.1.0';
const urlsToCache = [
'/',
'/index.html'
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0e87f2d..f48bbd4 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1058,6 +1058,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
]
},
allow_photos: true,
+ photo_requirement: 'optional',
max_photos: 3,
requires_comment_on_fail: false,
send_notification: false,
@@ -1149,6 +1150,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
]
},
allow_photos: true,
+ photo_requirement: 'optional',
max_photos: 3,
requires_comment_on_fail: false,
send_notification: false,
@@ -1182,6 +1184,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
]
},
allow_photos: question.allow_photos ?? true,
+ photo_requirement: question.photo_requirement || 'optional',
max_photos: question.max_photos || 3,
requires_comment_on_fail: question.requires_comment_on_fail || false,
send_notification: question.send_notification || false,
@@ -1212,6 +1215,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
points: parseInt(formData.points),
options: formData.options,
allow_photos: formData.allow_photos,
+ photo_requirement: formData.photo_requirement,
max_photos: parseInt(formData.max_photos),
requires_comment_on_fail: formData.requires_comment_on_fail,
send_notification: formData.send_notification,
@@ -1236,6 +1240,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
]
},
allow_photos: true,
+ photo_requirement: 'optional',
max_photos: 3,
requires_comment_on_fail: false,
send_notification: false,
@@ -1847,30 +1852,50 @@ function QuestionsManagerModal({ checklist, onClose }) {
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
-
- setFormData({ ...formData, allow_photos: e.target.checked })}
- className="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
- />
-
- Permitir fotos
-
-
-
-
- Máx. fotos
-
- setFormData({ ...formData, max_photos: parseInt(e.target.value) })}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- disabled={!formData.allow_photos}
- />
+
+
+ {/* Configuración de fotos/archivos */}
+
+
+ 📷 Fotos y Archivos Adjuntos
+
+
+
+
+ Requisito de adjuntos
+
+
setFormData({ ...formData, photo_requirement: e.target.value })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 bg-white"
+ >
+ 🚫 No permitir adjuntos
+ 📎 Opcional (puede adjuntar si quiere)
+ ⚠️ Obligatorio (debe adjuntar)
+
+
+ {formData.photo_requirement === 'none' && '• No se podrán adjuntar fotos/archivos'}
+ {formData.photo_requirement === 'optional' && '• El mecánico puede adjuntar si lo desea'}
+ {formData.photo_requirement === 'required' && '• El mecánico DEBE adjuntar al menos 1 archivo'}
+
+
+
+
+ Máx. archivos
+
+
setFormData({ ...formData, max_photos: parseInt(e.target.value) })}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ disabled={formData.photo_requirement === 'none'}
+ />
+
+ Cantidad máxima de fotos/PDFs permitidos
+
+
@@ -1992,7 +2017,15 @@ function QuestionsManagerModal({ checklist, onClose }) {
{question.type}
{question.points} pts
- {question.allow_photos && (
+ {question.photo_requirement === 'required' && (
+
+ ⚠️ Fotos obligatorias
+
+ )}
+ {question.photo_requirement === 'optional' && (
+ 📷 Máx {question.max_photos} fotos
+ )}
+ {(!question.photo_requirement || question.allow_photos) && !question.photo_requirement && (
📷 Máx {question.max_photos} fotos
)}
{question.send_notification && (
@@ -4396,12 +4429,20 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
const validateAllAnswered = () => {
const visibleQuestions = getVisibleQuestions()
const unanswered = visibleQuestions.filter(q => {
+ const answer = answers[q.id]
+
// Para preguntas tipo photo_only, solo validar que tenga fotos
if (q.options?.type === 'photo_only') {
- return !answers[q.id]?.photos?.length
+ return !answer?.photos?.length
}
+
+ // Validar fotos obligatorias (photo_requirement = 'required')
+ if (q.photo_requirement === 'required' && (!answer?.photos || answer.photos.length === 0)) {
+ return true // Falta adjuntar fotos obligatorias
+ }
+
// Para otros tipos, validar que tenga respuesta
- return !answers[q.id]?.value
+ return !answer?.value
})
return unanswered
}
@@ -5088,13 +5129,23 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
)}
{/* Photos */}
- {currentQuestion.allow_photos && (
+ {(currentQuestion.photo_requirement !== 'none' || currentQuestion.allow_photos) && (
- Fotografías / Documentos *
+ Fotografías / Documentos
+ {currentQuestion.photo_requirement === 'required' && (
+
+ ⚠️ OBLIGATORIO
+
+ )}
+ {currentQuestion.photo_requirement === 'optional' && (
+
+ (opcional)
+
+ )}
{currentQuestion.max_photos && (
- (máximo {currentQuestion.max_photos} archivo{currentQuestion.max_photos > 1 ? 's' : ''})
+ - máx {currentQuestion.max_photos} archivo{currentQuestion.max_photos > 1 ? 's' : ''}
)}
{(checklist.ai_mode === 'assisted' || checklist.ai_mode === 'full') && (
@@ -5115,7 +5166,7 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
disabled={aiAnalyzing}
- required={!answers[currentQuestion.id]?.photos?.length}
+ required={currentQuestion.photo_requirement === 'required' && !answers[currentQuestion.id]?.photos?.length}
/>
{/* Photo Previews */}
diff --git a/migrations/change_allow_photos_to_photo_requirement.sql b/migrations/change_allow_photos_to_photo_requirement.sql
new file mode 100644
index 0000000..3da0b9d
--- /dev/null
+++ b/migrations/change_allow_photos_to_photo_requirement.sql
@@ -0,0 +1,22 @@
+-- Migración: Cambiar allow_photos de Boolean a String con 3 estados
+-- Fecha: 2025-12-02
+-- Descripción: Agregar soporte para fotos opcionales/obligatorias/no permitidas
+
+-- Paso 1: Agregar nueva columna
+ALTER TABLE questions ADD COLUMN photo_requirement VARCHAR(20) DEFAULT 'optional';
+
+-- Paso 2: Migrar datos existentes
+UPDATE questions
+SET photo_requirement = CASE
+ WHEN allow_photos = TRUE THEN 'optional'
+ WHEN allow_photos = FALSE THEN 'none'
+ ELSE 'optional'
+END;
+
+-- Paso 3: Eliminar columna antigua (opcional, comentar si quieres mantener compatibilidad)
+-- ALTER TABLE questions DROP COLUMN allow_photos;
+
+-- Nota: Los valores válidos son:
+-- 'none' = No se permiten fotos
+-- 'optional' = Fotos opcionales (puede adjuntar o no)
+-- 'required' = Fotos obligatorias (debe adjuntar al menos 1)