Nueva Funcionalidad: 3 Estados para Adjuntos (Ninguno/Opcional/Obligatorio)

He implementado el sistema de 3 estados para el requisito de fotos/archivos que solicitaste.

Problema Original:
Solo había 2 estados:

 Permitir fotos (checkbox activado)
 No permitir fotos (checkbox desactivado)
Faltaba: Fotos opcionales vs obligatorias

Solución Implementada:
3 Estados disponibles:

🚫 No permitir adjuntos (photo_requirement = 'none')

No se muestra el input de fotos
El mecánico NO puede adjuntar archivos
📎 Opcional (photo_requirement = 'optional')

Se muestra el input de fotos
El mecánico PUEDE adjuntar si lo desea
No es obligatorio para continuar
⚠️ Obligatorio (photo_requirement = 'required')

Se muestra el input de fotos con etiqueta "OBLIGATORIO"
El mecánico DEBE adjuntar al menos 1 archivo
Validación bloquea continuar sin adjuntos
This commit is contained in:
2025-12-02 22:22:51 -03:00
parent 35b419a654
commit c4f5d960de
6 changed files with 109 additions and 35 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "checklist-frontend",
"private": true,
"version": "1.0.99",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -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'

View File

@@ -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"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={formData.allow_photos}
onChange={(e) => setFormData({ ...formData, allow_photos: e.target.checked })}
className="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
/>
<label className="ml-2 text-sm text-gray-700">
Permitir fotos
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Máx. fotos
</label>
<input
type="number"
min="0"
max="10"
value={formData.max_photos}
onChange={(e) => 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}
/>
</div>
{/* Configuración de fotos/archivos */}
<div className="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
<h4 className="text-sm font-semibold text-indigo-900 mb-3">
📷 Fotos y Archivos Adjuntos
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Requisito de adjuntos
</label>
<select
value={formData.photo_requirement || 'optional'}
onChange={(e) => 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"
>
<option value="none">🚫 No permitir adjuntos</option>
<option value="optional">📎 Opcional (puede adjuntar si quiere)</option>
<option value="required"> Obligatorio (debe adjuntar)</option>
</select>
<p className="text-xs text-gray-500 mt-1">
{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'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Máx. archivos
</label>
<input
type="number"
min="1"
max="10"
value={formData.max_photos}
onChange={(e) => 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'}
/>
<p className="text-xs text-gray-500 mt-1">
Cantidad máxima de fotos/PDFs permitidos
</p>
</div>
</div>
</div>
@@ -1992,7 +2017,15 @@ function QuestionsManagerModal({ checklist, onClose }) {
{question.type}
</span>
<span>{question.points} pts</span>
{question.allow_photos && (
{question.photo_requirement === 'required' && (
<span className="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
Fotos obligatorias
</span>
)}
{question.photo_requirement === 'optional' && (
<span>📷 Máx {question.max_photos} fotos</span>
)}
{(!question.photo_requirement || question.allow_photos) && !question.photo_requirement && (
<span>📷 Máx {question.max_photos} fotos</span>
)}
{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) && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Fotografías / Documentos *
Fotografías / Documentos
{currentQuestion.photo_requirement === 'required' && (
<span className="ml-2 text-xs text-red-600 font-semibold">
OBLIGATORIO
</span>
)}
{currentQuestion.photo_requirement === 'optional' && (
<span className="ml-2 text-xs text-gray-600">
(opcional)
</span>
)}
{currentQuestion.max_photos && (
<span className="ml-2 text-xs text-gray-600">
(máximo {currentQuestion.max_photos} archivo{currentQuestion.max_photos > 1 ? 's' : ''})
- máx {currentQuestion.max_photos} archivo{currentQuestion.max_photos > 1 ? 's' : ''}
</span>
)}
{(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 */}