Compare commits

...
2 Commits
3 changed files with 136 additions and 84 deletions

No files matched your search

+115 -27
View File
@@ -2,40 +2,58 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import StatsCard from "@/components/StatsCard"; 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 { useAuth } from "@/lib/auth";
import { formatDate } from "@/lib/format";
import Link from "next/link"; 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() { export default function DashboardPage() {
const { user, isLoading: authLoading } = useAuth(); const { user, isLoading: authLoading } = useAuth();
const router = useRouter();
const [stats, setStats] = useState<DashboardStats | null>(null); const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
if (authLoading) return; if (authLoading) return;
if (!canView) {
// Diferido para no disparar cascading renders (igual que AuthProvider).
queueMicrotask(() => setLoading(false));
return;
}
getTicketStats() getTicketStats()
.then(setStats) .then(setStats)
.catch(() => { .catch(() => {
setStats({ setStats(emptyStats);
totalTickets: 0,
openTickets: 0,
inProgressTickets: 0,
resolvedTickets: 0,
closedTickets: 0,
unassignedTickets: 0,
criticalTickets: 0,
dailyCounts: [],
});
}) })
.finally(() => setLoading(false)); .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) { if (authLoading || loading) {
return ( return (
@@ -45,20 +63,90 @@ export default function DashboardPage() {
); );
} }
// Solo administradores (todas las áreas) y supervisores (solo su área). // Usuario normal: sin mensaje de permisos, solo sus tickets.
if (!canView) { if (!isTeam) {
return ( return (
<div> <div>
<div className="page-header"> <div className="page-header">
<h1>Dashboard</h1> <h1>Mis Tickets</h1>
<Link href="/tickets" className="btn btn-primary"> <Link href="/tickets/new" className="btn btn-primary">
<i className="bi bi-ticket-detailed me-2"></i> <i className="bi bi-plus-lg me-2"></i>
Ir a Tickets Nuevo Ticket
</Link> </Link>
</div> </div>
<div className="alert alert-warning" role="alert">
<i className="bi bi-shield-lock me-2"></i> <div className="row g-3 mb-4">
El Dashboard está disponible solo para administradores y supervisores. <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>
</div> </div>
); );
+18 -52
View File
@@ -12,7 +12,6 @@ export default function NewTicketPage() {
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [priority, setPriority] = useState("Medium");
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const [groupId, setGroupId] = useState(""); const [groupId, setGroupId] = useState("");
@@ -29,7 +28,8 @@ export default function NewTicketPage() {
} }
setLoading(true); setLoading(true);
try { try {
const result = await createTicket({ title, description, priority, groupId: Number(groupId) }); // La prioridad siempre es Media al crear: la ajusta el equipo después.
const result = await createTicket({ title, description, priority: "Medium", groupId: Number(groupId) });
router.push(`/tickets/${result.id}`); router.push(`/tickets/${result.id}`);
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : "Error al crear el ticket"); setError(err instanceof Error ? err.message : "Error al crear el ticket");
@@ -50,7 +50,7 @@ export default function NewTicketPage() {
</div> </div>
<div className="row"> <div className="row">
<div className="col-lg-8"> <div className="col-12">
<div className="card"> <div className="card">
<div className="card-body p-4"> <div className="card-body p-4">
{error && ( {error && (
@@ -86,37 +86,21 @@ export default function NewTicketPage() {
></textarea> ></textarea>
</div> </div>
<div className="row"> <div className="mb-4">
<div className="col-md-6 mb-4"> <label className="form-label fw-semibold">Departamento</label>
<label className="form-label fw-semibold">Departamento</label> <select
<select className="form-select"
className="form-select" value={groupId}
value={groupId} onChange={(e) => setGroupId(e.target.value)}
onChange={(e) => setGroupId(e.target.value)} required
required >
> <option value="">Seleccionar departamento...</option>
<option value="">Seleccionar departamento...</option> {groups.map((g) => (
{groups.map((g) => ( <option key={g.id} value={g.id}>
<option key={g.id} value={g.id}> {g.name}
{g.name} </option>
</option> ))}
))} </select>
</select>
</div>
<div className="col-md-6 mb-4">
<label className="form-label fw-semibold">Prioridad</label>
<select
className="form-select"
value={priority}
onChange={(e) => setPriority(e.target.value)}
>
<option value="Low">Baja</option>
<option value="Medium">Media</option>
<option value="High">Alta</option>
<option value="Critical">Crítica</option>
</select>
</div>
</div> </div>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
@@ -145,24 +129,6 @@ export default function NewTicketPage() {
</div> </div>
</div> </div>
</div> </div>
{/* Tips */}
<div className="col-lg-4">
<div className="card">
<div className="card-body">
<h6 className="d-flex align-items-center gap-2 mb-3">
<i className="bi bi-lightbulb" style={{ color: "#ffc107" }}></i>
Consejos
</h6>
<ul className="mb-0" style={{ fontSize: "0.85rem", lineHeight: 1.7 }}>
<li>Sé específico en el asunto del ticket</li>
<li>Incluye pasos para reproducir el problema</li>
<li>Adjunta capturas de pantalla si es posible</li>
<li>Asigna la prioridad adecuada según el impacto</li>
</ul>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );
+3 -5
View File
@@ -9,11 +9,9 @@ export default function Sidebar() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navItems = [ const navItems = [
// Dashboard solo para administradores (todas las áreas) y // Dashboard para todos: el equipo ve el global (o su área) y los
// supervisores (solo su área). // usuarios normales ven solo sus tickets (sin mensaje de permisos).
...((user?.role === "admin" || user?.role === "supervisor") { href: "/", icon: "bi-speedometer2", label: "Dashboard" },
? [{ 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" },
]; ];