- 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)
171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { getTickets, type Ticket } from "@/lib/api";
|
|
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 [tickets, setTickets] = useState<Ticket[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [loading, setLoading] = useState(true);
|
|
const [statusFilter, setStatusFilter] = useState("");
|
|
const [priorityFilter, setPriorityFilter] = useState("");
|
|
const [search, setSearch] = useState("");
|
|
const pageSize = 10;
|
|
|
|
const fetchTickets = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getTickets({
|
|
status: statusFilter || undefined,
|
|
priority: priorityFilter || undefined,
|
|
search: search || undefined,
|
|
page,
|
|
pageSize,
|
|
});
|
|
setTickets(data.items || []);
|
|
setTotal(data.total || 0);
|
|
} catch {
|
|
setTickets([]);
|
|
setTotal(0);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [statusFilter, priorityFilter, search, page]);
|
|
|
|
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-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>
|
|
);
|
|
}
|