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
|
// Step 2: Auto-save answer when changed (non-blocking)
|
||||||
const handleAnswerSubmit = async () => {
|
const saveAnswer = async (questionId) => {
|
||||||
if (!inspectionId || currentQuestionIndex >= questions.length) return
|
if (!inspectionId) return
|
||||||
|
|
||||||
const question = questions[currentQuestionIndex]
|
const question = questions.find(q => q.id === questionId)
|
||||||
const answer = answers[question.id]
|
const answer = answers[questionId]
|
||||||
|
|
||||||
setLoading(true)
|
if (!answer?.value) return // Don't save empty answers
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
@@ -2038,8 +2038,6 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
is_flagged: status === 'critical'
|
is_flagged: status === 'critical'
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Submitting answer:', answerData)
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/api/answers`, {
|
const response = await fetch(`${API_URL}/api/answers`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2049,11 +2047,8 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
body: JSON.stringify(answerData)
|
body: JSON.stringify(answerData)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Answer response status:', response.status)
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const savedAnswer = await response.json()
|
const savedAnswer = await response.json()
|
||||||
console.log('Answer saved:', savedAnswer)
|
|
||||||
|
|
||||||
// Upload photos if any
|
// Upload photos if any
|
||||||
if (answer.photos.length > 0) {
|
if (answer.photos.length > 0) {
|
||||||
@@ -2069,25 +2064,57 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to next question or signatures
|
// Mark as saved
|
||||||
if (currentQuestionIndex < questions.length - 1) {
|
setAnswers({
|
||||||
setCurrentQuestionIndex(currentQuestionIndex + 1)
|
...answers,
|
||||||
} else {
|
[questionId]: { ...answers[questionId], saved: true }
|
||||||
setStep(3)
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.error('Error response:', errorText)
|
|
||||||
alert('Error al guardar respuesta: ' + errorText)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error)
|
console.error('Error saving answer:', error)
|
||||||
alert('Error al guardar respuesta: ' + error.message)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Step 3: Submit signatures and complete
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -2352,14 +2379,24 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{step === 1 && 'Datos del Vehículo'}
|
{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'}
|
{step === 3 && 'Firmas'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -2513,10 +2550,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
type="radio"
|
type="radio"
|
||||||
value="pass"
|
value="pass"
|
||||||
checked={answers[currentQuestion.id]?.value === 'pass'}
|
checked={answers[currentQuestion.id]?.value === 'pass'}
|
||||||
onChange={(e) => setAnswers({
|
onChange={(e) => {
|
||||||
...answers,
|
setAnswers({
|
||||||
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
|
...answers,
|
||||||
})}
|
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
|
||||||
|
})
|
||||||
|
setTimeout(() => saveAnswer(currentQuestion.id), 500)
|
||||||
|
}}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
<span className="text-green-600">✓ Pasa</span>
|
<span className="text-green-600">✓ Pasa</span>
|
||||||
@@ -2526,10 +2566,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
type="radio"
|
type="radio"
|
||||||
value="fail"
|
value="fail"
|
||||||
checked={answers[currentQuestion.id]?.value === 'fail'}
|
checked={answers[currentQuestion.id]?.value === 'fail'}
|
||||||
onChange={(e) => setAnswers({
|
onChange={(e) => {
|
||||||
...answers,
|
setAnswers({
|
||||||
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
|
...answers,
|
||||||
})}
|
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
|
||||||
|
})
|
||||||
|
setTimeout(() => saveAnswer(currentQuestion.id), 500)
|
||||||
|
}}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
<span className="text-red-600">✗ Falla</span>
|
<span className="text-red-600">✗ Falla</span>
|
||||||
@@ -2540,10 +2583,13 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
{currentQuestion.type === 'good_bad' && (
|
{currentQuestion.type === 'good_bad' && (
|
||||||
<select
|
<select
|
||||||
value={answers[currentQuestion.id]?.value}
|
value={answers[currentQuestion.id]?.value}
|
||||||
onChange={(e) => setAnswers({
|
onChange={(e) => {
|
||||||
...answers,
|
setAnswers({
|
||||||
[currentQuestion.id]: { ...answers[currentQuestion.id], value: e.target.value }
|
...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"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="">Seleccionar...</option>
|
<option value="">Seleccionar...</option>
|
||||||
@@ -2659,29 +2705,48 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
<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
|
<button
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => setCurrentQuestionIndex(currentQuestionIndex - 1)}
|
saveAnswer(currentQuestion.id)
|
||||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
|
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>
|
||||||
)}
|
)}
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Debug info */}
|
{/* Answer status indicator */}
|
||||||
{!answers[currentQuestion.id]?.value && (
|
{answers[currentQuestion.id]?.value && (
|
||||||
<div className="text-sm text-red-600 mt-2">
|
<div className="text-sm text-green-600 mt-2 flex items-center gap-1">
|
||||||
⚠️ Debe seleccionar una respuesta para continuar
|
<span>✓</span>
|
||||||
|
<span>Respuesta guardada automáticamente</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user