Files
helpdesk/frontend/src/app/(app)/tickets/page.tsx
T

208 lines
7.1 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { getTickets, type Ticket } from "@/lib/api";
import { useAuth } from "@/lib/auth";
import BadgeStatus from "@/components/BadgeStatus";
import BadgePriority from "@/components/BadgePriority";
import PaginationComponent from "@/components/Pagination";
import Link from "next/link";
export default function TicketsPage() {
const router = useRouter();
const { user } = useAuth();
const [tickets, setTickets] = useState<Ticket[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [view, setView] = useState<"all" | "mine" | "assigned" | "unassigned">("all");
const [statusFilter, setStatusFilter] = useState("");
const [priorityFilter, setPriorityFilter] = useState("");
const [search, setSearch] = useState("");
const pageSize = 10;
const numericId = parseInt(user?.idUsuario ?? "", 10);
const hasNumericId = !isNaN(numericId);
const fetchTickets = useCallback(async () => {
setLoading(true);
try {
const data = await getTickets({
status: statusFilter || undefined,
priority: priorityFilter || undefined,
requesterId: view === "mine" && hasNumericId ? numericId : undefined,
assigneeId:
view === "assigned" && hasNumericId
? numericId
: view === "unassigned"
? 0
: undefined,
search: search || undefined,
page,
pageSize,
});
setTickets(data.items || []);
setTotal(data.total || 0);
} catch {
setTickets([]);
setTotal(0);
} finally {
setLoading(false);
}
}, [statusFilter, priorityFilter, search, page, view, numericId, hasNumericId]);
useEffect(() => {
fetchTickets();
}, [fetchTickets]);
const totalPages = Math.ceil(total / pageSize);
return (
<div>
<div className="page-header">
<h1>Tickets</h1>
<Link href="/tickets/new" className="btn btn-primary">
<i className="bi bi-plus-lg me-2"></i>
Nuevo Ticket
</Link>
</div>
{/* Filters */}
<div className="filter-bar mb-3">
<div className="btn-group" role="group" aria-label="Vista">
{(
[
["all", "Todos"],
["mine", "Míos"],
["assigned", "Asignados a mí"],
["unassigned", "Sin asignar"],
] as const
).map(([v, label]) => (
<button
key={v}
type="button"
className={`btn btn-sm ${view === v ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => {
setView(v);
setPage(1);
}}
>
{label}
</button>
))}
</div>
</div>
<div className="filter-bar mb-4">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-funnel text-muted"></i>
<span className="fw-semibold" style={{ fontSize: "0.85rem" }}>Filtros:</span>
</div>
<select
className="form-select"
style={{ width: "auto" }}
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
>
<option value="">Todos los estados</option>
<option value="Open">Abierto</option>
<option value="Pending">Pendiente</option>
<option value="InProgress">En Progreso</option>
<option value="Resolved">Resuelto</option>
<option value="Closed">Cerrado</option>
</select>
<select
className="form-select"
style={{ width: "auto" }}
value={priorityFilter}
onChange={(e) => { setPriorityFilter(e.target.value); setPage(1); }}
>
<option value="">Todas las prioridades</option>
<option value="Low">Baja</option>
<option value="Medium">Media</option>
<option value="High">Alta</option>
<option value="Critical">Crítica</option>
</select>
<div className="position-relative flex-grow-1" style={{ maxWidth: "280px" }}>
<i className="bi bi-search position-absolute" style={{ left: "10px", top: "50%", transform: "translateY(-50%)", color: "#6c757d" }}></i>
<input
type="text"
className="form-control"
style={{ paddingLeft: "2rem" }}
placeholder="Buscar tickets..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
</div>
</div>
{/* Tickets Table */}
<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>Asignado</th>
<th>Actualizado</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={7} className="text-center py-4">
<div className="spinner-border spinner-border-sm text-primary" role="status"></div>
</td>
</tr>
) : tickets.length === 0 ? (
<tr>
<td colSpan={7} className="empty-state">
<i className="bi bi-inbox d-block"></i>
<h5>No se encontraron tickets</h5>
<p className="text-muted mb-0">Prueba ajustando los filtros de búsqueda</p>
</td>
</tr>
) : (
tickets.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>{ticket.requesterName || "N/A"}</td>
<td><BadgeStatus status={ticket.status} /></td>
<td><BadgePriority priority={ticket.priority} /></td>
<td>{ticket.assigneeName || <span className="text-muted">Sin asignar</span>}</td>
<td className="text-muted" style={{ fontSize: "0.85rem" }}>
{new Date(ticket.updatedAt).toLocaleDateString("es-CL")}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="pagination-wrapper">
<span className="pagination-info">
Mostrando {tickets.length} de {total} tickets
</span>
<PaginationComponent currentPage={page} totalPages={totalPages} onPageChange={setPage} />
</div>
</div>
);
}