- 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)
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import StatsCard from "@/components/StatsCard";
|
|
import BadgeStatus from "@/components/BadgeStatus";
|
|
import { getTicketStats, type DashboardStats } from "@/lib/api";
|
|
import Link from "next/link";
|
|
|
|
export default function DashboardPage() {
|
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
getTicketStats()
|
|
.then(setStats)
|
|
.catch(() => {
|
|
setStats({
|
|
totalTickets: 0,
|
|
openTickets: 0,
|
|
inProgressTickets: 0,
|
|
resolvedTickets: 0,
|
|
closedTickets: 0,
|
|
unassignedTickets: 0,
|
|
criticalTickets: 0,
|
|
dailyCounts: [],
|
|
});
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="loading-spinner">
|
|
<div className="spinner-border text-primary" role="status"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const maxDaily = Math.max(...(stats?.dailyCounts?.map((d) => d.count) || [1]));
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-header">
|
|
<h1>Dashboard</h1>
|
|
<Link href="/tickets/new" className="btn btn-primary">
|
|
<i className="bi bi-plus-lg me-2"></i>
|
|
Nuevo Ticket
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="row g-3 mb-4">
|
|
<div className="col-6 col-lg">
|
|
<StatsCard icon="bi-ticket-detailed" value={stats?.totalTickets || 0} label="Total Tickets" color="#00285d" />
|
|
</div>
|
|
<div className="col-6 col-lg">
|
|
<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?.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-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>
|
|
|
|
{/* 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 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>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|