feat: Implement non-blocking inspection flow with conditional questions - WIP

This commit is contained in:
2025-11-21 00:51:19 -03:00
parent c3ae68da4f
commit 45086e8922

View File

@@ -2005,14 +2005,14 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
}
}
// Step 2: Submit answer and move to next question
const handleAnswerSubmit = async () => {
if (!inspectionId || currentQuestionIndex >= questions.length) return
// Step 2: Auto-save answer when changed (non-blocking)
const saveAnswer = async (questionId) => {
if (!inspectionId) return
const question = questions[currentQuestionIndex]
const answer = answers[question.id]
const question = questions.find(q => q.id === questionId)
const answer = answers[questionId]
setLoading(true)
if (!answer?.value) return // Don't save empty answers
try {
const token = localStorage.getItem('token')
@@ -2038,8 +2038,6 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
is_flagged: status === 'critical'
}
console.log('Submitting answer:', answerData)
const response = await fetch(`${API_URL}/api/answers`, {
method: 'POST',
headers: {
@@ -2049,11 +2047,8 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
body: JSON.stringify(answerData)
})
console.log('Answer response status:', response.status)
if (response.ok) {
const savedAnswer = await response.json()
console.log('Answer saved:', savedAnswer)
// Upload photos if any
if (answer.photos.length > 0) {
@@ -2069,25 +2064,57 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
}
}
// Move to next question or signatures
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex(currentQuestionIndex + 1)
} else {
setStep(3)
}
} else {
const errorText = await response.text()
console.error('Error response:', errorText)
alert('Error al guardar respuesta: ' + errorText)
// Mark as saved
setAnswers({
...answers,
[questionId]: { ...answers[questionId], saved: true }
})
}
} catch (error) {
console.error('Error:', error)
alert('Error al guardar respuesta: ' + error.message)
} finally {
setLoading(false)
console.error('Error saving answer:', error)
}
}
// Navigate between questions freely
const goToQuestion = (index) => {
setCurrentQuestionIndex(index)
}
// Validate all questions answered before completing
const validateAllAnswered = () => {
const visibleQuestions = getVisibleQuestions()
const unanswered = visibleQuestions.filter(q => !answers[q.id]?.value)
return unanswered
}
// Get visible questions based on conditional logic
const getVisibleQuestions = () => {
return questions.filter(q => {
// If no parent, always visible
if (!q.parent_question_id) return true
// Check parent answer
const parentAnswer = answers[q.parent_question_id]
if (!parentAnswer) return false
// Show if parent answer matches trigger
return parentAnswer.value === q.show_if_answer
})
}
// Move to signatures step
const proceedToSignatures = () => {
const unanswered = validateAllAnswered()
if (unanswered.length > 0) {
alert(`⚠️ Faltan responder ${unanswered.length} pregunta(s). Por favor completa todas las preguntas antes de continuar.`)
// Go to first unanswered
const firstIndex = questions.findIndex(q => q.id === unanswered[0].id)
if (firstIndex >= 0) setCurrentQuestionIndex(firstIndex)
return
}
setStep(3)
}
// Step 3: Submit signatures and complete
const handleComplete = async () => {
setLoading(true)
@@ -2352,14 +2379,24 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
</span>
<span className="text-sm text-gray-500">
{step === 1 && 'Datos del Vehículo'}
{step === 2 && `Pregunta ${currentQuestionIndex + 1} de ${questions.length}`}
{step === 2 && (() => {
const visible = getVisibleQuestions()
const answered = visible.filter(q => answers[q.id]?.value).length
return `${answered}/${visible.length} preguntas respondidas`
})()}
{step === 3 && 'Firmas'}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${(step / 3) * 100}%` }}
style={{
width: step === 1 ? '33%' : step === 2 ? (() => {
const visible = getVisibleQuestions()
const answered = visible.filter(q => answers[q.id]?.value).length
return `${33 + (answered / visible.length) * 33}%`
})() : '100%'
}}
/>
</div>
</div>
@@ -2513,10 +2550,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
type="radio"
value="pass"
checked={answers[currentQuestion.id]?.value === 'pass'}
onChange={(e) => setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})}
onChange={(e) => {
setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="mr-2"
/>
<span className="text-green-600"> Pasa</span>
@@ -2526,10 +2566,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
type="radio"
value="fail"
checked={answers[currentQuestion.id]?.value === 'fail'}
onChange={(e) => setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})}
onChange={(e) => {
setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="mr-2"
/>
<span className="text-red-600"> Falla</span>
@@ -2540,10 +2583,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
{currentQuestion.type === 'good_bad' && (
<select
value={answers[currentQuestion.id]?.value}
onChange={(e) => setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})}
onChange={(e) => {
setAnswers({
...answers,
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
})
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Seleccionar...</option>
@@ -2659,29 +2705,48 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
</div>
<div className="flex gap-3 pt-4">
{currentQuestionIndex > 0 && (
<button
type="button"
onClick={() => {
if (currentQuestionIndex > 0) {
saveAnswer(currentQuestion.id)
goToQuestion(currentQuestionIndex - 1)
}
}}
disabled={currentQuestionIndex === 0}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Anterior
</button>
{currentQuestionIndex < getVisibleQuestions().length - 1 ? (
<button
type="button"
onClick={() => setCurrentQuestionIndex(currentQuestionIndex - 1)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
onClick={() => {
saveAnswer(currentQuestion.id)
goToQuestion(currentQuestionIndex + 1)
}}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Anterior
Siguiente
</button>
) : (
<button
onClick={() => {
saveAnswer(currentQuestion.id)
proceedToSignatures()
}}
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
>
Completar y Firmar
</button>
)}
<button
onClick={handleAnswerSubmit}
disabled={loading || !answers[currentQuestion.id]?.value}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
title={!answers[currentQuestion.id]?.value ? 'Debe seleccionar una respuesta' : ''}
>
{loading ? 'Guardando...' : currentQuestionIndex < questions.length - 1 ? 'Siguiente →' : 'Continuar a Firmas'}
</button>
</div>
{/* Debug info */}
{!answers[currentQuestion.id]?.value && (
<div className="text-sm text-red-600 mt-2">
Debe seleccionar una respuesta para continuar
{/* Answer status indicator */}
{answers[currentQuestion.id]?.value && (
<div className="text-sm text-green-600 mt-2 flex items-center gap-1">
<span></span>
<span>Respuesta guardada automáticamente</span>
</div>
)}
</div>