feat: Implement non-blocking inspection flow with conditional questions - WIP
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user