Backend v1.0.73:
- Implementado sistema de reordenamiento de preguntas
- Nuevo endpoint PATCH /api/checklists/{id}/questions/reorder
- Schema QuestionReorder para validar datos de reorden
- Actualización en lote de campo 'order' en preguntas
- Auditoría automática de cambios de orden
- Validación de permisos y existencia de checklist
Frontend v1.0.71:
- Agregada funcionalidad de reordenamiento de preguntas
- Botones ▲ ▼ para mover preguntas arriba/abajo
- Función moveQuestion() para gestionar reordenamiento
- Interfaz visual mejorada con separadores
- Tooltips descriptivos en botones de orden
- Recarga automática tras reordenar
This commit is contained in:
@@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
# No lanzamos excepción para no interrumpir el flujo normal
|
# No lanzamos excepción para no interrumpir el flujo normal
|
||||||
|
|
||||||
|
|
||||||
BACKEND_VERSION = "1.0.72"
|
BACKEND_VERSION = "1.0.73"
|
||||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -1041,6 +1041,52 @@ def update_question(
|
|||||||
return db_question
|
return db_question
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/api/checklists/{checklist_id}/questions/reorder")
|
||||||
|
def reorder_questions(
|
||||||
|
checklist_id: int,
|
||||||
|
reorder_data: List[schemas.QuestionReorder],
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Reordenar preguntas de un checklist"""
|
||||||
|
if current_user.role != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="No autorizado")
|
||||||
|
|
||||||
|
# Verificar que el checklist existe
|
||||||
|
checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first()
|
||||||
|
if not checklist:
|
||||||
|
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
||||||
|
|
||||||
|
# Actualizar el orden de cada pregunta
|
||||||
|
for item in reorder_data:
|
||||||
|
question = db.query(models.Question).filter(
|
||||||
|
models.Question.id == item.question_id,
|
||||||
|
models.Question.checklist_id == checklist_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if question:
|
||||||
|
old_order = question.order
|
||||||
|
question.order = item.new_order
|
||||||
|
question.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
# Registrar auditoría
|
||||||
|
audit_log = models.QuestionAuditLog(
|
||||||
|
question_id=question.id,
|
||||||
|
checklist_id=checklist_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
action="updated",
|
||||||
|
field_name="order",
|
||||||
|
old_value=str(old_order),
|
||||||
|
new_value=str(item.new_order),
|
||||||
|
comment="Orden de pregunta actualizado"
|
||||||
|
)
|
||||||
|
db.add(audit_log)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {"message": "Orden de preguntas actualizado exitosamente", "updated_count": len(reorder_data)}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/questions/{question_id}")
|
@app.delete("/api/questions/{question_id}")
|
||||||
def delete_question(
|
def delete_question(
|
||||||
question_id: int,
|
question_id: int,
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ class QuestionCreate(QuestionBase):
|
|||||||
class QuestionUpdate(QuestionBase):
|
class QuestionUpdate(QuestionBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class QuestionReorder(BaseModel):
|
||||||
|
question_id: int
|
||||||
|
new_order: int
|
||||||
|
|
||||||
class Question(QuestionBase):
|
class Question(QuestionBase):
|
||||||
id: int
|
id: int
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.70",
|
"version": "1.0.71",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1167,6 +1167,51 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moveQuestion = async (questionId, direction) => {
|
||||||
|
const questionsList = Object.values(questionsBySection).flat()
|
||||||
|
const currentIndex = questionsList.findIndex(q => q.id === questionId)
|
||||||
|
|
||||||
|
if (currentIndex === -1) return
|
||||||
|
|
||||||
|
const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
|
||||||
|
|
||||||
|
if (newIndex < 0 || newIndex >= questionsList.length) return
|
||||||
|
|
||||||
|
// Crear nueva lista con el orden actualizado
|
||||||
|
const newList = [...questionsList]
|
||||||
|
const [movedQuestion] = newList.splice(currentIndex, 1)
|
||||||
|
newList.splice(newIndex, 0, movedQuestion)
|
||||||
|
|
||||||
|
// Preparar datos para el backend
|
||||||
|
const reorderData = newList.map((q, index) => ({
|
||||||
|
question_id: q.id,
|
||||||
|
new_order: index
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/api/checklists/${checklist.id}/questions/reorder`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(reorderData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadQuestions()
|
||||||
|
} else {
|
||||||
|
alert('Error al reordenar pregunta')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
alert('Error al reordenar pregunta')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const questionsBySection = questions.reduce((acc, q) => {
|
const questionsBySection = questions.reduce((acc, q) => {
|
||||||
const section = q.section || 'Sin sección'
|
const section = q.section || 'Sin sección'
|
||||||
if (!acc[section]) acc[section] = []
|
if (!acc[section]) acc[section] = []
|
||||||
@@ -1582,7 +1627,25 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex gap-2">
|
<div className="ml-4 flex gap-2 items-center">
|
||||||
|
{/* Botones de reordenamiento */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => moveQuestion(question.id, 'up')}
|
||||||
|
className="text-gray-500 hover:text-indigo-600 text-xs px-2 py-1 hover:bg-indigo-50 rounded transition"
|
||||||
|
title="Mover arriba"
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => moveQuestion(question.id, 'down')}
|
||||||
|
className="text-gray-500 hover:text-indigo-600 text-xs px-2 py-1 hover:bg-indigo-50 rounded transition"
|
||||||
|
title="Mover abajo"
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-px bg-gray-300"></div>
|
||||||
<button
|
<button
|
||||||
onClick={() => loadAuditHistory(question.id)}
|
onClick={() => loadAuditHistory(question.id)}
|
||||||
className="text-gray-600 hover:text-gray-700 text-sm"
|
className="text-gray-600 hover:text-gray-700 text-sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user