Adapt frontend to SAM API: RUT auth, new types, helpdesk endpoints

- Login: RUT-based (SAM auth) instead of email
- API client: updated all endpoints to /api/helpdesk/*
- Types: match SAM response format (nombre, apPaterno, perfil)
- Dashboard: uses new DashboardStats with dailyCounts
- Tickets: title instead of subject, requesterName/assigneeName
- KB: categoryName/authorName instead of nested objects
- Removed register page (SAM manages users)
This commit is contained in:
Nurfog committed 2026-09-10 11:27:35 -03:00
1 parent 0cca57f21e
commit f56f6365d2
11 files changed
+285 -718

No files matched your search

+37 -140
View File
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import StatsCard from "@/components/StatsCard";
import BadgeStatus from "@/components/BadgeStatus";
import { getDashboardStats, type DashboardStats } from "@/lib/api";
import { getTicketStats, type DashboardStats } from "@/lib/api";
import Link from "next/link";
export default function DashboardPage() {
@@ -11,25 +11,18 @@ export default function DashboardPage() {
const [loading, setLoading] = useState(true);
useEffect(() => {
getDashboardStats()
getTicketStats()
.then(setStats)
.catch(() => {
// Set mock data for demo
setStats({
totalTickets: 128,
openTickets: 24,
pendingTickets: 18,
resolvedTickets: 62,
closedTickets: 24,
avgResponseTime: "2.4h",
ticketsByPriority: { low: 30, medium: 45, high: 38, urgent: 15 },
recentTickets: [],
agentWorkload: [
{ agent: "Carlos Méndez", count: 12 },
{ agent: "Ana García", count: 9 },
{ agent: "Roberto Díaz", count: 7 },
{ agent: "María López", count: 5 },
],
totalTickets: 0,
openTickets: 0,
inProgressTickets: 0,
resolvedTickets: 0,
closedTickets: 0,
unassignedTickets: 0,
criticalTickets: 0,
dailyCounts: [],
});
})
.finally(() => setLoading(false));
@@ -43,8 +36,7 @@ export default function DashboardPage() {
);
}
const maxPriority = Math.max(...Object.values(stats?.ticketsByPriority || {}), 1);
const maxWorkload = Math.max(...(stats?.agentWorkload?.map((a) => a.count) || [1]));
const maxDaily = Math.max(...(stats?.dailyCounts?.map((d) => d.count) || [1]));
return (
<div>
@@ -65,140 +57,45 @@ export default function DashboardPage() {
<StatsCard icon="bi-folder2-open" value={stats?.openTickets || 0} label="Abiertos" color="#198754" />
</div>
<div className="col-6 col-lg">
<StatsCard icon="bi-clock-history" value={stats?.pendingTickets || 0} label="Pendientes" color="#ffc107" />
<StatsCard icon="bi-clock-history" value={stats?.inProgressTickets || 0} label="En Progreso" color="#ffc107" />
</div>
<div className="col-6 col-lg">
<StatsCard icon="bi-check-circle" value={stats?.resolvedTickets || 0} label="Resueltos" color="#0d6efd" />
</div>
<div className="col-6 col-lg">
<StatsCard icon="bi-speedometer" value={stats?.avgResponseTime || "N/A"} label="Tiempo Respuesta" color="#6f42c1" />
<StatsCard icon="bi-exclamation-triangle" value={stats?.criticalTickets || 0} label="Críticos" color="#dc3545" />
</div>
<div className="col-6 col-lg">
<StatsCard icon="bi-person-dash" value={stats?.unassignedTickets || 0} label="Sin Asignar" color="#6f42c1" />
</div>
</div>
<div className="row g-4">
{/* Priority Distribution */}
<div className="col-lg-6">
<div className="card h-100">
<div className="card-header d-flex align-items-center gap-2">
<i className="bi bi-bar-chart" style={{ color: "var(--primary)" }}></i>
Tickets por Prioridad
</div>
<div className="card-body">
{Object.entries(stats?.ticketsByPriority || {}).map(([priority, count]) => {
const labels: Record<string, string> = { low: "Baja", medium: "Media", high: "Alta", urgent: "Urgente" };
const colors: Record<string, string> = { low: "#6c757d", medium: "#0d6efd", high: "#fd7e14", urgent: "#dc3545" };
return (
<div key={priority} className="mb-3">
<div className="d-flex justify-content-between mb-1">
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>{labels[priority] || priority}</span>
<span className="text-muted" style={{ fontSize: "0.85rem" }}>{count}</span>
</div>
<div className="progress" style={{ height: "8px", borderRadius: "4px" }}>
<div
className="progress-bar"
role="progressbar"
style={{
width: `${(count / maxPriority) * 100}%`,
backgroundColor: colors[priority],
borderRadius: "4px",
}}
></div>
</div>
</div>
);
})}
</div>
{/* Daily Tickets Chart */}
{stats?.dailyCounts && stats.dailyCounts.length > 0 && (
<div className="card mb-4">
<div className="card-header d-flex align-items-center gap-2">
<i className="bi bi-graph-up" style={{ color: "var(--primary)" }}></i>
Tickets por Día (últimos 30 días)
</div>
</div>
{/* Agent Workload */}
<div className="col-lg-6">
<div className="card h-100">
<div className="card-header d-flex align-items-center gap-2">
<i className="bi bi-people" style={{ color: "var(--primary)" }}></i>
Carga de Agentes
</div>
<div className="card-body">
{stats?.agentWorkload?.map((item) => (
<div key={item.agent} className="mb-3">
<div className="d-flex justify-content-between mb-1">
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>{item.agent}</span>
<span className="text-muted" style={{ fontSize: "0.85rem" }}>{item.count} tickets</span>
</div>
<div className="progress" style={{ height: "8px", borderRadius: "4px" }}>
<div
className="progress-bar"
role="progressbar"
style={{
width: `${(item.count / maxWorkload) * 100}%`,
background: "var(--primary-gradient)",
borderRadius: "4px",
}}
></div>
</div>
</div>
<div className="card-body">
<div className="d-flex align-items-end gap-1" style={{ height: "120px" }}>
{stats.dailyCounts.map((d) => (
<div
key={d.date}
className="flex-fill rounded"
style={{
height: `${(d.count / maxDaily) * 100}%`,
backgroundColor: "var(--primary)",
opacity: 0.7,
minHeight: "4px",
}}
title={`${d.date}: ${d.count} tickets`}
></div>
))}
{(!stats?.agentWorkload || stats.agentWorkload.length === 0) && (
<div className="text-center text-muted py-4">
<i className="bi bi-people fs-1 mb-2 d-block" style={{ opacity: 0.3 }}></i>
Sin datos de agentes disponibles
</div>
)}
</div>
</div>
</div>
</div>
{/* Recent Tickets */}
<div className="card mt-4">
<div className="card-header d-flex align-items-center justify-content-between">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-clock-history" style={{ color: "var(--primary)" }}></i>
Tickets Recientes
</div>
<Link href="/tickets" className="btn btn-sm btn-outline-primary">
Ver todos
</Link>
</div>
<div className="table-responsive">
<table className="table table-modern mb-0">
<thead>
<tr>
<th>#</th>
<th>Asunto</th>
<th>Solicitante</th>
<th>Estado</th>
<th>Prioridad</th>
<th>Actualizado</th>
</tr>
</thead>
<tbody>
{stats?.recentTickets?.length ? (
stats.recentTickets.map((ticket) => (
<tr key={ticket.id} onClick={() => window.location.href = `/tickets/${ticket.id}`}>
<td className="fw-semibold" style={{ color: "var(--primary)" }}>
{ticket.ticketNumber || `#${ticket.id}`}
</td>
<td>{ticket.subject}</td>
<td>{ticket.requester?.name || "N/A"}</td>
<td><BadgeStatus status={ticket.status} /></td>
<td><span className={`badge-priority badge-${ticket.priority}`}>{ticket.priority}</span></td>
<td className="text-muted">{new Date(ticket.updatedAt).toLocaleDateString("es-CL")}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="text-center text-muted py-4">
<i className="bi bi-inbox fs-3 d-block mb-2" style={{ opacity: 0.3 }}></i>
No hay tickets recientes
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}