develop #1
@@ -1554,6 +1554,240 @@ def get_inspections_report(
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/inspections/{inspection_id}/pdf")
|
||||
def export_inspection_to_pdf(
|
||||
inspection_id: int,
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Exportar inspección a PDF con imágenes"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
# Obtener inspección
|
||||
inspection = db.query(models.Inspection).filter(
|
||||
models.Inspection.id == inspection_id
|
||||
).first()
|
||||
|
||||
if not inspection:
|
||||
raise HTTPException(status_code=404, detail="Inspección no encontrada")
|
||||
|
||||
# Verificar permisos (admin o mecánico dueño)
|
||||
if current_user.role != "admin" and inspection.mechanic_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="No tienes permisos para ver esta inspección")
|
||||
|
||||
# Obtener datos relacionados
|
||||
checklist = db.query(models.Checklist).filter(models.Checklist.id == inspection.checklist_id).first()
|
||||
mechanic = db.query(models.User).filter(models.User.id == inspection.mechanic_id).first()
|
||||
answers = db.query(models.Answer).options(
|
||||
joinedload(models.Answer.media_files)
|
||||
).join(models.Question).filter(
|
||||
models.Answer.inspection_id == inspection_id
|
||||
).order_by(models.Question.section, models.Question.order).all()
|
||||
|
||||
# Crear PDF en memoria
|
||||
buffer = BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=30, leftMargin=30, topMargin=30, bottomMargin=30)
|
||||
|
||||
# Contenedor para elementos del PDF
|
||||
elements = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Estilos personalizados
|
||||
title_style = ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=styles['Heading1'],
|
||||
fontSize=24,
|
||||
textColor=colors.HexColor('#4338ca'),
|
||||
spaceAfter=30,
|
||||
alignment=TA_CENTER
|
||||
)
|
||||
|
||||
heading_style = ParagraphStyle(
|
||||
'CustomHeading',
|
||||
parent=styles['Heading2'],
|
||||
fontSize=16,
|
||||
textColor=colors.HexColor('#4338ca'),
|
||||
spaceAfter=12,
|
||||
spaceBefore=12
|
||||
)
|
||||
|
||||
# Título
|
||||
elements.append(Paragraph("REPORTE DE INSPECCIÓN", title_style))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Información general
|
||||
info_data = [
|
||||
['Checklist:', checklist.name if checklist else 'N/A'],
|
||||
['Mecánico:', mechanic.full_name if mechanic else 'N/A'],
|
||||
['Fecha:', inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else 'N/A'],
|
||||
['Estado:', inspection.status.upper()],
|
||||
['', ''],
|
||||
['Vehículo', ''],
|
||||
['Patente:', inspection.vehicle_plate or 'N/A'],
|
||||
['Marca:', inspection.vehicle_brand or 'N/A'],
|
||||
['Modelo:', inspection.vehicle_model or 'N/A'],
|
||||
['Kilometraje:', f"{inspection.vehicle_km:,} km" if inspection.vehicle_km else 'N/A'],
|
||||
['Cliente:', inspection.client_name or 'N/A'],
|
||||
['OR Número:', inspection.or_number or 'N/A'],
|
||||
]
|
||||
|
||||
if inspection.status == 'completed' and inspection.score is not None:
|
||||
info_data.insert(4, ['Score:', f"{inspection.score}/{inspection.max_score} ({inspection.percentage:.1f}%)"])
|
||||
info_data.insert(5, ['Items Señalados:', str(inspection.flagged_items_count)])
|
||||
|
||||
info_table = Table(info_data, colWidths=[2*inch, 4*inch])
|
||||
info_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#e0e7ff')),
|
||||
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#4338ca')),
|
||||
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 8),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
]))
|
||||
|
||||
elements.append(info_table)
|
||||
elements.append(Spacer(1, 30))
|
||||
|
||||
# Respuestas por sección
|
||||
if answers:
|
||||
elements.append(Paragraph("RESULTADOS DE INSPECCIÓN", heading_style))
|
||||
elements.append(Spacer(1, 10))
|
||||
|
||||
current_section = None
|
||||
|
||||
for answer in answers:
|
||||
question = answer.question
|
||||
|
||||
# Nueva sección
|
||||
if question.section != current_section:
|
||||
if current_section is not None:
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
current_section = question.section
|
||||
section_style = ParagraphStyle(
|
||||
'Section',
|
||||
parent=styles['Heading3'],
|
||||
fontSize=14,
|
||||
textColor=colors.HexColor('#6366f1'),
|
||||
spaceAfter=10
|
||||
)
|
||||
elements.append(Paragraph(f"● {current_section}", section_style))
|
||||
|
||||
# Datos de la pregunta
|
||||
answer_color = colors.white
|
||||
if answer.is_flagged:
|
||||
answer_color = colors.HexColor('#fee2e2')
|
||||
elif answer.answer_value in ['pass', 'good']:
|
||||
answer_color = colors.HexColor('#dcfce7')
|
||||
elif answer.answer_value in ['fail', 'bad']:
|
||||
answer_color = colors.HexColor('#fee2e2')
|
||||
|
||||
answer_text = answer.answer_value or answer.answer_text or 'N/A'
|
||||
if answer.points_earned is not None:
|
||||
answer_text += f" ({answer.points_earned} pts)"
|
||||
|
||||
question_data = [
|
||||
[Paragraph(f"<b>{question.text}</b>", styles['Normal']), answer_text.upper()]
|
||||
]
|
||||
|
||||
if answer.comment:
|
||||
question_data.append([Paragraph(f"<i>Comentarios: {answer.comment}</i>", styles['Normal']), ''])
|
||||
|
||||
question_table = Table(question_data, colWidths=[4*inch, 2*inch])
|
||||
question_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), answer_color),
|
||||
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
||||
('ALIGN', (1, 0), (1, 0), 'CENTER'),
|
||||
('FONTNAME', (1, 0), (1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 9),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
]))
|
||||
|
||||
elements.append(question_table)
|
||||
elements.append(Spacer(1, 5))
|
||||
|
||||
# Fotos adjuntas
|
||||
if answer.media_files and len(answer.media_files) > 0:
|
||||
elements.append(Spacer(1, 5))
|
||||
photos_per_row = 2
|
||||
photo_width = 2.5 * inch
|
||||
photo_height = 2 * inch
|
||||
|
||||
for i in range(0, len(answer.media_files), photos_per_row):
|
||||
photo_row = []
|
||||
for media_file in answer.media_files[i:i+photos_per_row]:
|
||||
try:
|
||||
photo_path = media_file.file_path
|
||||
# Si la foto es base64
|
||||
if photo_path.startswith('data:image'):
|
||||
img_data = base64.b64decode(photo_path.split(',')[1])
|
||||
img_buffer = BytesIO(img_data)
|
||||
img = RLImage(img_buffer, width=photo_width, height=photo_height)
|
||||
else:
|
||||
# Si es una ruta de archivo
|
||||
full_path = os.path.join(os.getcwd(), photo_path)
|
||||
if os.path.exists(full_path):
|
||||
img = RLImage(full_path, width=photo_width, height=photo_height)
|
||||
else:
|
||||
continue
|
||||
photo_row.append(img)
|
||||
except Exception as e:
|
||||
print(f"Error loading image: {e}")
|
||||
continue
|
||||
|
||||
if photo_row:
|
||||
photo_table = Table([photo_row])
|
||||
photo_table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
]))
|
||||
elements.append(photo_table)
|
||||
elements.append(Spacer(1, 10))
|
||||
|
||||
else:
|
||||
elements.append(Paragraph("No hay respuestas registradas", styles['Normal']))
|
||||
|
||||
# Pie de página
|
||||
elements.append(Spacer(1, 30))
|
||||
footer_style = ParagraphStyle(
|
||||
'Footer',
|
||||
parent=styles['Normal'],
|
||||
fontSize=8,
|
||||
textColor=colors.grey,
|
||||
alignment=TA_CENTER
|
||||
)
|
||||
elements.append(Paragraph(f"Generado por Syntria - {datetime.now().strftime('%d/%m/%Y %H:%M')}", footer_style))
|
||||
|
||||
# Construir PDF
|
||||
doc.build(elements)
|
||||
|
||||
# Preparar respuesta
|
||||
buffer.seek(0)
|
||||
filename = f"inspeccion_{inspection_id}_{inspection.vehicle_plate or 'sin-patente'}.pdf"
|
||||
|
||||
return StreamingResponse(
|
||||
buffer,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============= HEALTH CHECK =============
|
||||
@app.get("/")
|
||||
def root():
|
||||
|
||||
@@ -339,10 +339,7 @@ function DashboardPage({ user, setUser }) {
|
||||
) : activeTab === 'users' ? (
|
||||
<UsersTab user={user} />
|
||||
) : activeTab === 'reports' ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-xl text-gray-400">📊</div>
|
||||
<div className="text-gray-500 mt-2">Módulo de Reportes en desarrollo...</div>
|
||||
</div>
|
||||
<ReportsTab user={user} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1732,6 +1729,38 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate }) {
|
||||
{/* Footer */}
|
||||
<div className="border-t p-4 bg-gray-50">
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
const response = await fetch(`${API_URL}/api/inspections/${inspection.id}/pdf`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} else {
|
||||
alert('Error al generar PDF')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
alert('Error al generar PDF')
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center gap-2"
|
||||
>
|
||||
<span>📄</span>
|
||||
Exportar PDF
|
||||
</button>
|
||||
{user?.role === 'admin' && (
|
||||
<>
|
||||
<button
|
||||
@@ -3142,5 +3171,272 @@ function UsersTab({ user }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Componente de Reportes
|
||||
function ReportsTab({ user }) {
|
||||
const [dashboardData, setDashboardData] = useState(null)
|
||||
const [inspections, setInspections] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
mechanicId: ''
|
||||
})
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
let url = `${API_URL}/api/reports/dashboard?`
|
||||
if (filters.startDate) url += `start_date=${filters.startDate}&`
|
||||
if (filters.endDate) url += `end_date=${filters.endDate}&`
|
||||
if (filters.mechanicId) url += `mechanic_id=${filters.mechanicId}&`
|
||||
|
||||
console.log('Loading dashboard from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
console.log('Dashboard response status:', response.status)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
console.log('Dashboard data:', data)
|
||||
setDashboardData(data)
|
||||
} else {
|
||||
const errorText = await response.text()
|
||||
console.error('Dashboard error response:', response.status, errorText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadInspections = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
let url = `${API_URL}/api/reports/inspections?`
|
||||
if (filters.startDate) url += `start_date=${filters.startDate}&`
|
||||
if (filters.endDate) url += `end_date=${filters.endDate}&`
|
||||
if (filters.mechanicId) url += `mechanic_id=${filters.mechanicId}&`
|
||||
|
||||
console.log('Loading inspections from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
console.log('Inspections response status:', response.status)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
console.log('Inspections data:', data)
|
||||
setInspections(data)
|
||||
} else {
|
||||
const errorText = await response.text()
|
||||
console.error('Inspections error response:', response.status, errorText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading inspections:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
await Promise.all([loadDashboard(), loadInspections()])
|
||||
setLoading(false)
|
||||
}
|
||||
loadData()
|
||||
}, [filters])
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-12">Cargando reportes...</div>
|
||||
}
|
||||
|
||||
if (!dashboardData || !dashboardData.stats) {
|
||||
return <div className="text-center py-12">No hay datos disponibles</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Filtros */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fecha Inicio
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.startDate}
|
||||
onChange={(e) => setFilters({...filters, startDate: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fecha Fin
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.endDate}
|
||||
onChange={(e) => setFilters({...filters, endDate: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
{user.role === 'admin' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mecánico
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="ID del mecánico"
|
||||
value={filters.mechanicId}
|
||||
onChange={(e) => setFilters({...filters, mechanicId: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Métricas principales */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-500 mb-1">Total Inspecciones</div>
|
||||
<div className="text-3xl font-bold text-indigo-600">
|
||||
{dashboardData.stats?.total_inspections || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-500 mb-1">Completadas</div>
|
||||
<div className="text-3xl font-bold text-green-600">
|
||||
{dashboardData.stats?.completed_inspections || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-500 mb-1">Tasa de Completado</div>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{(dashboardData.stats?.completion_rate || 0).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="text-sm text-gray-500 mb-1">Promedio Score</div>
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{(dashboardData.stats?.avg_score || 0).toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ranking de Mecánicos */}
|
||||
{user.role === 'admin' && dashboardData.mechanic_ranking?.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">🏆 Ranking de Mecánicos</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 px-4">Posición</th>
|
||||
<th className="text-left py-2 px-4">Mecánico</th>
|
||||
<th className="text-right py-2 px-4">Inspecciones</th>
|
||||
<th className="text-right py-2 px-4">Promedio</th>
|
||||
<th className="text-right py-2 px-4">% Completado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboardData.mechanic_ranking.map((mechanic, index) => (
|
||||
<tr key={mechanic.mechanic_id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 px-4">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`}
|
||||
</td>
|
||||
<td className="py-2 px-4 font-medium">{mechanic.mechanic_name}</td>
|
||||
<td className="py-2 px-4 text-right">{mechanic.total_inspections}</td>
|
||||
<td className="py-2 px-4 text-right">{mechanic.avg_score.toFixed(1)}</td>
|
||||
<td className="py-2 px-4 text-right">{mechanic.completion_rate.toFixed(1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Estadísticas por Checklist */}
|
||||
{dashboardData.checklist_stats?.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">📋 Estadísticas por Checklist</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{dashboardData.checklist_stats.map((checklist) => (
|
||||
<div key={checklist.checklist_id} className="border rounded-lg p-4">
|
||||
<div className="font-medium text-gray-900 mb-2">{checklist.checklist_name}</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<div>Inspecciones: {checklist.total_inspections}</div>
|
||||
<div>Promedio: {checklist.avg_score.toFixed(1)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lista de Inspecciones */}
|
||||
{inspections.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">📝 Inspecciones Recientes</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 px-4">Fecha</th>
|
||||
<th className="text-left py-2 px-4">Checklist</th>
|
||||
<th className="text-left py-2 px-4">Mecánico</th>
|
||||
<th className="text-left py-2 px-4">Placa</th>
|
||||
<th className="text-right py-2 px-4">Score</th>
|
||||
<th className="text-center py-2 px-4">Estado</th>
|
||||
<th className="text-center py-2 px-4">Alertas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inspections.slice(0, 20).map((inspection) => (
|
||||
<tr key={inspection.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 px-4">
|
||||
{new Date(inspection.started_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="py-2 px-4">{inspection.checklist_name}</td>
|
||||
<td className="py-2 px-4">{inspection.mechanic_name}</td>
|
||||
<td className="py-2 px-4 font-mono">{inspection.vehicle_plate}</td>
|
||||
<td className="py-2 px-4 text-right font-medium">{inspection.score}</td>
|
||||
<td className="py-2 px-4 text-center">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
inspection.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{inspection.status === 'completed' ? 'Completada' : 'Pendiente'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-4 text-center">
|
||||
{inspection.flagged_items > 0 && (
|
||||
<span className="px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
|
||||
🚩 {inspection.flagged_items}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default App
|
||||
|
||||
Reference in New Issue
Block a user