feat: dashboard solo para el equipo, usuarios van a tickets; altos uniformes
This commit is contained in:
1 parent
dedab95cc5
commit
c702a19c1e
3 files changed
+42
-131
No files matched your search
+25
-128
@@ -2,15 +2,32 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import StatsCard from "@/components/StatsCard";
|
import StatsCard from "@/components/StatsCard";
|
||||||
import BadgeStatus from "@/components/BadgeStatus";
|
import { getTicketStats, type DashboardStats } from "@/lib/api";
|
||||||
import BadgePriority from "@/components/BadgePriority";
|
|
||||||
import { getTicketStats, getTickets, type DashboardStats, type Ticket } from "@/lib/api";
|
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { formatDate } from "@/lib/format";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
const emptyStats: DashboardStats = {
|
export default function DashboardPage() {
|
||||||
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Dashboard solo para el equipo. Los usuarios normales solo ven
|
||||||
|
// Tickets y Base de Conocimientos: se les redirige a sus tickets.
|
||||||
|
const isTeam =
|
||||||
|
user?.role === "admin" || user?.role === "agent" || user?.role === "supervisor";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authLoading) return;
|
||||||
|
if (!isTeam) {
|
||||||
|
router.replace("/tickets");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getTicketStats()
|
||||||
|
.then(setStats)
|
||||||
|
.catch(() => {
|
||||||
|
setStats({
|
||||||
totalTickets: 0,
|
totalTickets: 0,
|
||||||
openTickets: 0,
|
openTickets: 0,
|
||||||
inProgressTickets: 0,
|
inProgressTickets: 0,
|
||||||
@@ -19,43 +36,12 @@ const emptyStats: DashboardStats = {
|
|||||||
unassignedTickets: 0,
|
unassignedTickets: 0,
|
||||||
criticalTickets: 0,
|
criticalTickets: 0,
|
||||||
dailyCounts: [],
|
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);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
getTicketStats()
|
|
||||||
.then(setStats)
|
|
||||||
.catch(() => {
|
|
||||||
setStats(emptyStats);
|
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
if (!isTeam) {
|
}, [authLoading, isTeam, router]);
|
||||||
getTickets({ page: 1, pageSize: 10 })
|
|
||||||
.then((data) => {
|
|
||||||
setMyTickets(data.items || []);
|
|
||||||
setMyTotal(data.total || 0);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setMyTickets([]);
|
|
||||||
setMyTotal(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [authLoading, isTeam]);
|
|
||||||
|
|
||||||
if (authLoading || loading) {
|
if (authLoading || loading || !isTeam) {
|
||||||
return (
|
return (
|
||||||
<div className="loading-spinner">
|
<div className="loading-spinner">
|
||||||
<div className="spinner-border text-primary" role="status"></div>
|
<div className="spinner-border text-primary" role="status"></div>
|
||||||
@@ -63,95 +49,6 @@ export default function DashboardPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usuario normal: sin mensaje de permisos, solo sus tickets.
|
|
||||||
if (!isTeam) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="page-header">
|
|
||||||
<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="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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxDaily = Math.max(...(stats?.dailyCounts?.map((d) => d.count) || [1]));
|
const maxDaily = Math.max(...(stats?.dailyCounts?.map((d) => d.count) || [1]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card:hover {
|
.stats-card:hover {
|
||||||
@@ -693,6 +694,13 @@ body {
|
|||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Botones de la barra de filtros al mismo alto que selects e inputs */
|
||||||
|
.filter-bar .btn {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ticket Detail */
|
/* Ticket Detail */
|
||||||
.ticket-header {
|
.ticket-header {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -855,6 +863,7 @@ body {
|
|||||||
/* Dashboard: tarjetas clicables que filtran tickets */
|
/* Dashboard: tarjetas clicables que filtran tickets */
|
||||||
.stats-card-anchor {
|
.stats-card-anchor {
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 100%;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ export default function Sidebar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
|
const isTeam =
|
||||||
|
user?.role === "admin" || user?.role === "agent" || user?.role === "supervisor";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
// Dashboard para todos: el equipo ve el global (o su área) y los
|
// Dashboard solo para el equipo. Los usuarios normales solo ven
|
||||||
// usuarios normales ven solo sus tickets (sin mensaje de permisos).
|
// Tickets y Base de Conocimientos.
|
||||||
{ href: "/", icon: "bi-speedometer2", label: "Dashboard" },
|
...(isTeam
|
||||||
|
? [{ href: "/", icon: "bi-speedometer2", label: "Dashboard" }]
|
||||||
|
: []),
|
||||||
{ href: "/tickets", icon: "bi-ticket-detailed", label: "Tickets" },
|
{ href: "/tickets", icon: "bi-ticket-detailed", label: "Tickets" },
|
||||||
{ href: "/knowledge-base", icon: "bi-journal-bookmark", label: "Knowledge Base" },
|
{ href: "/knowledge-base", icon: "bi-journal-bookmark", label: "Knowledge Base" },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in new issue
Block a user