feat: dashboard visible para usuarios normales con solo sus tickets
This commit is contained in:
1 parent
f9dc520f65
commit
baef1cc5d6
2 files changed
+118
-32
No files matched your search
+115
-27
@@ -2,40 +2,58 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import StatsCard from "@/components/StatsCard";
|
||||
import { getTicketStats, type DashboardStats } from "@/lib/api";
|
||||
import BadgeStatus from "@/components/BadgeStatus";
|
||||
import BadgePriority from "@/components/BadgePriority";
|
||||
import { getTicketStats, getTickets, type DashboardStats, type Ticket } from "@/lib/api";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const emptyStats: DashboardStats = {
|
||||
totalTickets: 0,
|
||||
openTickets: 0,
|
||||
inProgressTickets: 0,
|
||||
resolvedTickets: 0,
|
||||
closedTickets: 0,
|
||||
unassignedTickets: 0,
|
||||
criticalTickets: 0,
|
||||
dailyCounts: [],
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, isLoading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [myTickets, setMyTickets] = useState<Ticket[]>([]);
|
||||
const [myTotal, setMyTotal] = useState(0);
|
||||
|
||||
const canView = user?.role === "admin" || user?.role === "supervisor";
|
||||
// Equipo (ve todo o su área) vs usuario normal (solo sus tickets).
|
||||
// El backend ya filtra stats y tickets por solicitante cuando no es equipo.
|
||||
const isTeam =
|
||||
user?.role === "admin" || user?.role === "agent" || user?.role === "supervisor";
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
if (!canView) {
|
||||
// Diferido para no disparar cascading renders (igual que AuthProvider).
|
||||
queueMicrotask(() => setLoading(false));
|
||||
return;
|
||||
}
|
||||
getTicketStats()
|
||||
.then(setStats)
|
||||
.catch(() => {
|
||||
setStats({
|
||||
totalTickets: 0,
|
||||
openTickets: 0,
|
||||
inProgressTickets: 0,
|
||||
resolvedTickets: 0,
|
||||
closedTickets: 0,
|
||||
unassignedTickets: 0,
|
||||
criticalTickets: 0,
|
||||
dailyCounts: [],
|
||||
});
|
||||
setStats(emptyStats);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [authLoading, canView]);
|
||||
if (!isTeam) {
|
||||
getTickets({ page: 1, pageSize: 10 })
|
||||
.then((data) => {
|
||||
setMyTickets(data.items || []);
|
||||
setMyTotal(data.total || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
setMyTickets([]);
|
||||
setMyTotal(0);
|
||||
});
|
||||
}
|
||||
}, [authLoading, isTeam]);
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
@@ -45,20 +63,90 @@ export default function DashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Solo administradores (todas las áreas) y supervisores (solo su área).
|
||||
if (!canView) {
|
||||
// Usuario normal: sin mensaje de permisos, solo sus tickets.
|
||||
if (!isTeam) {
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<Link href="/tickets" className="btn btn-primary">
|
||||
<i className="bi bi-ticket-detailed me-2"></i>
|
||||
Ir a Tickets
|
||||
<h1>Mis Tickets</h1>
|
||||
<Link href="/tickets/new" className="btn btn-primary">
|
||||
<i className="bi bi-plus-lg me-2"></i>
|
||||
Nuevo Ticket
|
||||
</Link>
|
||||
</div>
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<i className="bi bi-shield-lock me-2"></i>
|
||||
El Dashboard está disponible solo para administradores y supervisores.
|
||||
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-6 col-lg">
|
||||
<StatsCard icon="bi-ticket-detailed" value={stats?.totalTickets || 0} label="Mis Tickets" color="#00285d" href="/tickets" />
|
||||
</div>
|
||||
<div className="col-6 col-lg">
|
||||
<StatsCard icon="bi-folder2-open" value={stats?.openTickets || 0} label="Abiertos" color="#198754" href="/tickets?status=Open" />
|
||||
</div>
|
||||
<div className="col-6 col-lg">
|
||||
<StatsCard icon="bi-clock-history" value={stats?.inProgressTickets || 0} label="En Progreso" color="#ffc107" href="/tickets?status=InProgress" />
|
||||
</div>
|
||||
<div className="col-6 col-lg">
|
||||
<StatsCard icon="bi-check-circle" value={stats?.resolvedTickets || 0} label="Resueltos" color="#0d6efd" href="/tickets?status=Resolved" />
|
||||
</div>
|
||||
<div className="col-6 col-lg">
|
||||
<StatsCard icon="bi-exclamation-triangle" value={stats?.criticalTickets || 0} label="Críticos" color="#dc3545" href="/tickets?priority=Critical" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header d-flex align-items-center justify-content-between">
|
||||
<span>
|
||||
<i className="bi bi-ticket-detailed me-2" style={{ color: "var(--primary)" }}></i>
|
||||
Últimos tickets ({myTotal})
|
||||
</span>
|
||||
<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>Estado</th>
|
||||
<th>Prioridad</th>
|
||||
<th>Actualizado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{myTickets.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="empty-state">
|
||||
<i className="bi bi-inbox d-block"></i>
|
||||
<h5>Sin tickets</h5>
|
||||
<p className="text-muted mb-0">Aún no has creado ningún ticket</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
myTickets.map((ticket) => (
|
||||
<tr
|
||||
key={ticket.id}
|
||||
onClick={() => router.push(`/tickets/${ticket.id}`)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<td className="fw-semibold" style={{ color: "var(--primary)" }}>
|
||||
{ticket.ticketNumber || `#${ticket.id}`}
|
||||
</td>
|
||||
<td>
|
||||
<div className="fw-medium">{ticket.title}</div>
|
||||
</td>
|
||||
<td><BadgeStatus status={ticket.status} /></td>
|
||||
<td><BadgePriority priority={ticket.priority} /></td>
|
||||
<td className="text-muted" style={{ fontSize: "0.85rem" }}>
|
||||
{formatDate(ticket.updatedAt)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user