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:
1 parent
0cca57f21e
commit
f56f6365d2
11 files changed
+285
-718
No files matched your search
+37
-140
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user