Corregir notificaicon check
This commit is contained in:
@@ -25,6 +25,9 @@ class Settings(BaseSettings):
|
|||||||
# Environment
|
# Environment
|
||||||
ENVIRONMENT: str = "development"
|
ENVIRONMENT: str = "development"
|
||||||
|
|
||||||
|
# Notificaciones
|
||||||
|
NOTIFICACION_ENDPOINT: str = ""
|
||||||
|
|
||||||
# CORS - Orígenes permitidos separados por coma
|
# CORS - Orígenes permitidos separados por coma
|
||||||
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,70 @@ from app import models, schemas
|
|||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import sys
|
import sys
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Función para enviar notificaciones al webhook
|
||||||
|
def send_answer_notification(answer, question, mechanic, db):
|
||||||
|
"""Envía notificación al webhook cuando se responde una pregunta marcada"""
|
||||||
|
try:
|
||||||
|
if not app_config.settings.NOTIFICACION_ENDPOINT:
|
||||||
|
print("No hay endpoint de notificación configurado")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Obtener datos de la inspección
|
||||||
|
inspection = db.query(models.Inspection).filter(
|
||||||
|
models.Inspection.id == answer.inspection_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not inspection:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Preparar datos para enviar
|
||||||
|
notification_data = {
|
||||||
|
"tipo": "respuesta_pregunta",
|
||||||
|
"pregunta": {
|
||||||
|
"id": question.id,
|
||||||
|
"texto": question.text,
|
||||||
|
"seccion": question.section
|
||||||
|
},
|
||||||
|
"respuesta": {
|
||||||
|
"id": answer.id,
|
||||||
|
"valor": answer.answer_value,
|
||||||
|
"estado": answer.status,
|
||||||
|
"comentario": answer.comment,
|
||||||
|
"puntos": answer.points_earned
|
||||||
|
},
|
||||||
|
"inspeccion": {
|
||||||
|
"id": inspection.id,
|
||||||
|
"vehiculo_placa": inspection.vehicle_plate,
|
||||||
|
"vehiculo_marca": inspection.vehicle_brand,
|
||||||
|
"vehiculo_modelo": inspection.vehicle_model,
|
||||||
|
"cliente": inspection.client_name,
|
||||||
|
"or_number": inspection.or_number
|
||||||
|
},
|
||||||
|
"mecanico": {
|
||||||
|
"id": mechanic.id,
|
||||||
|
"nombre": mechanic.full_name,
|
||||||
|
"email": mechanic.email
|
||||||
|
},
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enviar al webhook
|
||||||
|
response = requests.post(
|
||||||
|
app_config.settings.NOTIFICACION_ENDPOINT,
|
||||||
|
json=notification_data,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ Notificación enviada para pregunta {question.id}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Error al enviar notificación: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error enviando notificación: {e}")
|
||||||
|
# No lanzamos excepción para no interrumpir el flujo normal
|
||||||
|
|
||||||
BACKEND_VERSION = "1.0.25"
|
BACKEND_VERSION = "1.0.25"
|
||||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
@@ -941,6 +1005,11 @@ def create_answer(
|
|||||||
existing_answer.updated_at = datetime.utcnow()
|
existing_answer.updated_at = datetime.utcnow()
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(existing_answer)
|
db.refresh(existing_answer)
|
||||||
|
|
||||||
|
# Enviar notificación si la pregunta lo requiere
|
||||||
|
if question.send_notification:
|
||||||
|
send_answer_notification(existing_answer, question, current_user, db)
|
||||||
|
|
||||||
return existing_answer
|
return existing_answer
|
||||||
else:
|
else:
|
||||||
# Si status es pass/fail y no hay valor, no poner valor por defecto en answer_value
|
# Si status es pass/fail y no hay valor, no poner valor por defecto en answer_value
|
||||||
@@ -954,6 +1023,11 @@ def create_answer(
|
|||||||
db.add(db_answer)
|
db.add(db_answer)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_answer)
|
db.refresh(db_answer)
|
||||||
|
|
||||||
|
# Enviar notificación si la pregunta lo requiere
|
||||||
|
if question.send_notification:
|
||||||
|
send_answer_notification(db_answer, question, current_user, db)
|
||||||
|
|
||||||
return db_answer
|
return db_answer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class Question(Base):
|
|||||||
allow_photos = Column(Boolean, default=True)
|
allow_photos = Column(Boolean, default=True)
|
||||||
max_photos = Column(Integer, default=3)
|
max_photos = Column(Integer, default=3)
|
||||||
requires_comment_on_fail = Column(Boolean, default=False)
|
requires_comment_on_fail = Column(Boolean, default=False)
|
||||||
|
send_notification = Column(Boolean, default=False)
|
||||||
|
|
||||||
# Conditional logic
|
# Conditional logic
|
||||||
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
|
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class QuestionBase(BaseModel):
|
|||||||
allow_photos: bool = True
|
allow_photos: bool = True
|
||||||
max_photos: int = 3
|
max_photos: int = 3
|
||||||
requires_comment_on_fail: bool = False
|
requires_comment_on_fail: bool = False
|
||||||
|
send_notification: bool = False
|
||||||
parent_question_id: Optional[int] = None
|
parent_question_id: Optional[int] = None
|
||||||
show_if_answer: Optional[str] = None
|
show_if_answer: Optional[str] = None
|
||||||
ai_prompt: Optional[str] = None
|
ai_prompt: Optional[str] = None
|
||||||
|
|||||||
@@ -908,6 +908,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
allow_photos: true,
|
allow_photos: true,
|
||||||
max_photos: 3,
|
max_photos: 3,
|
||||||
requires_comment_on_fail: false,
|
requires_comment_on_fail: false,
|
||||||
|
send_notification: false,
|
||||||
parent_question_id: null,
|
parent_question_id: null,
|
||||||
show_if_answer: '',
|
show_if_answer: '',
|
||||||
ai_prompt: ''
|
ai_prompt: ''
|
||||||
@@ -966,6 +967,7 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
allow_photos: true,
|
allow_photos: true,
|
||||||
max_photos: 3,
|
max_photos: 3,
|
||||||
requires_comment_on_fail: false,
|
requires_comment_on_fail: false,
|
||||||
|
send_notification: false,
|
||||||
parent_question_id: null,
|
parent_question_id: null,
|
||||||
show_if_answer: '',
|
show_if_answer: '',
|
||||||
ai_prompt: ''
|
ai_prompt: ''
|
||||||
@@ -1221,6 +1223,23 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.send_notification}
|
||||||
|
onChange={(e) => setFormData({ ...formData, send_notification: e.target.checked })}
|
||||||
|
className="w-4 h-4 text-yellow-600 border-gray-300 rounded focus:ring-yellow-500"
|
||||||
|
/>
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
🔔 Enviar notificación cuando se responda esta pregunta
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-2 ml-6">
|
||||||
|
Si activas esta opción, se enviará una notificación automática al administrador cada vez que un mecánico responda esta pregunta.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
|
className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
|
||||||
@@ -3472,12 +3491,14 @@ function ReportsTab({ user }) {
|
|||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
date: '',
|
date: '',
|
||||||
mechanicId: '',
|
mechanicId: '',
|
||||||
checklistId: ''
|
checklistId: '',
|
||||||
|
vehiclePlate: ''
|
||||||
})
|
})
|
||||||
const [appliedFilters, setAppliedFilters] = useState({
|
const [appliedFilters, setAppliedFilters] = useState({
|
||||||
date: '',
|
date: '',
|
||||||
mechanicId: '',
|
mechanicId: '',
|
||||||
checklistId: ''
|
checklistId: '',
|
||||||
|
vehiclePlate: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadMechanics = async () => {
|
const loadMechanics = async () => {
|
||||||
@@ -3572,6 +3593,14 @@ function ReportsTab({ user }) {
|
|||||||
data = filtered
|
data = filtered
|
||||||
console.log('After checklist filter:', data.length, 'items')
|
console.log('After checklist filter:', data.length, 'items')
|
||||||
}
|
}
|
||||||
|
// Filtrar por matrícula en el frontend
|
||||||
|
if (filtersToApply.vehiclePlate) {
|
||||||
|
const filtered = data.filter(i =>
|
||||||
|
i.vehicle_plate && i.vehicle_plate.toLowerCase().includes(filtersToApply.vehiclePlate.toLowerCase())
|
||||||
|
)
|
||||||
|
data = filtered
|
||||||
|
console.log('After plate filter:', data.length, 'items')
|
||||||
|
}
|
||||||
setInspections(data)
|
setInspections(data)
|
||||||
} else {
|
} else {
|
||||||
console.error('Inspections request failed:', response.status)
|
console.error('Inspections request failed:', response.status)
|
||||||
@@ -3594,7 +3623,7 @@ function ReportsTab({ user }) {
|
|||||||
// Cargar mecánicos y checklists primero
|
// Cargar mecánicos y checklists primero
|
||||||
await Promise.all([loadMechanics(), loadChecklists()])
|
await Promise.all([loadMechanics(), loadChecklists()])
|
||||||
// Luego cargar datos sin filtros (filtros vacíos = todos los datos)
|
// Luego cargar datos sin filtros (filtros vacíos = todos los datos)
|
||||||
const emptyFilters = { date: '', mechanicId: '', checklistId: '' }
|
const emptyFilters = { date: '', mechanicId: '', checklistId: '', vehiclePlate: '' }
|
||||||
await Promise.all([loadDashboard(emptyFilters), loadInspections(emptyFilters)])
|
await Promise.all([loadDashboard(emptyFilters), loadInspections(emptyFilters)])
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -3614,7 +3643,7 @@ function ReportsTab({ user }) {
|
|||||||
{/* Filtros */}
|
{/* Filtros */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">🔍 Filtros</h3>
|
<h3 className="text-lg font-semibold mb-4">🔍 Filtros</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Fecha
|
Fecha
|
||||||
@@ -3662,6 +3691,18 @@ function ReportsTab({ user }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Matrícula
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={filters.vehiclePlate}
|
||||||
|
onChange={(e) => setFilters({...filters, vehiclePlate: e.target.value})}
|
||||||
|
placeholder="Ej: ABC123"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
onClick={applyFilters}
|
onClick={applyFilters}
|
||||||
|
|||||||
Reference in New Issue
Block a user