From 24dcb6d77049c15e566578e88ae22528794fe74d Mon Sep 17 00:00:00 2001 From: Nurfog Date: Thu, 22 Oct 2026 11:19:25 -0300 Subject: [PATCH] =?UTF-8?q?feat(tickets):=20filtro=20Todos/Activos/Archiva?= =?UTF-8?q?dos=20+=20bot=C3=B3n=20archivar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- frontend/src/app/(app)/tickets/[id]/page.tsx | 35 ++++++++++++ frontend/src/app/(app)/tickets/page.tsx | 60 +++++++++++++++++++- frontend/src/lib/api.ts | 10 ++++ 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d6aede..ccfcabe 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ push a `main` → webhook Gitea → build + up en `.4` Base: `NEXT_PUBLIC_API_URL/api/helpdesk` - `POST /login {idUsuario, password}` · `GET /me` -- `GET /tickets?...` · `GET /ticket/{id}` · `POST /tickets` · `PUT /ticket/{id}` (+ `/status`, `/assign`) · `DELETE /ticket/{id}` +- `GET /tickets?...` (con `archived=false` solo activos [default UI], `true` solo archivados, ausente todos) · `GET /ticket/{id}` · `POST /tickets` · `PUT /ticket/{id}` (+ `/status`, `/assign`, `/archive`) · `DELETE /ticket/{id}` - `GET|POST /comments/{ticketId}` - `GET /kb/published` · `GET /kb/search?q=` · `GET /kb/categories` - `GET /stats` diff --git a/frontend/src/app/(app)/tickets/[id]/page.tsx b/frontend/src/app/(app)/tickets/[id]/page.tsx index 2eaa47c..21e5044 100644 --- a/frontend/src/app/(app)/tickets/[id]/page.tsx +++ b/frontend/src/app/(app)/tickets/[id]/page.tsx @@ -9,6 +9,7 @@ import { getTicketEmails, addComment, updateTicketStatus, + setTicketArchived, assignTicket, getGroups, getTeam, @@ -286,6 +287,24 @@ export default function TicketDetailPage() { } }; + const handleArchiveToggle = async () => { + if (!ticket) return; + const toArchived = !ticket.archived; + if (toArchived && !window.confirm("¿Archivar este ticket? Saldrá de la vista de activos, conservando su estado.")) { + return; + } + setError(""); + setActing(true); + try { + await setTicketArchived(id, toArchived); + await refreshTicket(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "No se pudo cambiar el archivado"); + } finally { + setActing(false); + } + }; + const handleAssignAgent = async (idUsuario: string | null) => { if (!ticket) return; setAssignOpen(false); @@ -338,6 +357,11 @@ export default function TicketDetailPage() {
+ {ticket.archived && ( + + Archivado + + )}
{isTeam && ( @@ -437,6 +461,17 @@ export default function TicketDetailPage() { )} + + {/* Archivar / Desarchivar (conserva el estado del ticket) */} + )} diff --git a/frontend/src/app/(app)/tickets/page.tsx b/frontend/src/app/(app)/tickets/page.tsx index 6da59c6..799537e 100644 --- a/frontend/src/app/(app)/tickets/page.tsx +++ b/frontend/src/app/(app)/tickets/page.tsx @@ -12,6 +12,19 @@ import Link from "next/link"; type View = "all" | "mine" | "assigned" | "unassigned"; +// Alcance de archivado: por defecto solo activos. "all" = todos, +// "active" = no archivados, "archived" = solo archivados. +type ArchiveScope = "all" | "active" | "archived"; + +function parseArchiveScope(v: string | null): ArchiveScope | null { + if (v === null) return null; + const k = v.trim().toLowerCase(); + if (k === "all" || k === "todos") return "all"; + if (k === "active" || k === "activos" || k === "false" || k === "0") return "active"; + if (k === "archived" || k === "archivados" || k === "true" || k === "1") return "archived"; + return null; +} + const STATUS_OPTIONS = [ { value: "Open", label: "Abierto" }, { value: "Pending", label: "Pendiente" }, @@ -64,6 +77,7 @@ function TicketsContent() { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(""); const [view, setView] = useState("all"); + const [archiveScope, setArchiveScope] = useState("active"); // Vacío = Todos los estados. Con 1 o más = filtra por esos estados. const [statusFilters, setStatusFilters] = useState([]); const [statusOpen, setStatusOpen] = useState(false); @@ -98,6 +112,7 @@ function TicketsContent() { unique.length >= STATUS_OPTIONS.length ? [] : unique; const priority = searchParams.get("priority") ?? ""; const viewParam = searchParams.get("view") as View | null; + const archiveParam = parseArchiveScope(searchParams.get("archived")); const q = searchParams.get("search") ?? ""; queueMicrotask(() => { setStatusFilters(normalized); @@ -105,6 +120,8 @@ function TicketsContent() { if (viewParam === "mine" || viewParam === "assigned" || viewParam === "unassigned" || viewParam === "all") { setView(viewParam); } + // Sin ?archived en la URL el default es solo activos. + setArchiveScope(archiveParam ?? "active"); setSearch(q); setPage(1); }); @@ -134,6 +151,7 @@ function TicketsContent() { : view === "unassigned" ? 0 : undefined, + archived: archiveScope === "all" ? undefined : archiveScope === "archived", search: search || undefined, page, pageSize, @@ -148,10 +166,14 @@ function TicketsContent() { setLoadError(err instanceof Error ? err.message : "Error al cargar tickets"); }) .finally(() => setLoading(false)); - }, [statusFilters, priorityFilter, search, page, view, numericId, hasNumericId, isTeam]); + }, [statusFilters, priorityFilter, search, page, view, archiveScope, numericId, hasNumericId, isTeam]); const totalPages = Math.ceil(total / pageSize); - const hasActiveFilters = statusFilters.length > 0 || priorityFilter !== "" || view !== "all" || search !== ""; + const hasActiveFilters = statusFilters.length > 0 || priorityFilter !== "" || view !== "all" || search !== "" || archiveScope !== "active"; + + function archiveScopeLabel(s: ArchiveScope): string { + return s === "all" ? "Todos" : s === "archived" ? "Archivados" : "Activos"; + } const toggleStatus = (value: string) => { setStatusFilters((prev) => { @@ -182,6 +204,7 @@ function TicketsContent() { setStatusOpen(false); setPriorityFilter(""); setView("all"); + setArchiveScope("active"); setSearch(""); setPage(1); router.replace("/tickets"); @@ -226,6 +249,7 @@ function TicketsContent() { {statusFilters.length > 0 && <> · Estado: {statusFilters.map(statusLabel).join(", ")}} {priorityFilter && <> · Prioridad: {priorityFilter}} {view !== "all" && <> · Vista: {view === "unassigned" ? "Sin asignar" : view}} + {archiveScope !== "active" && <> · Archivo: {archiveScopeLabel(archiveScope)}} {search && <> · Búsqueda: {search}} {" "}(desde el Dashboard) @@ -238,6 +262,7 @@ function TicketsContent() { {/* Filters */} {isTeam ? (
+ Vista:
{( [ @@ -260,6 +285,28 @@ function TicketsContent() { ))}
+ Archivo: +
+ {( + [ + ["all", "Todos"], + ["active", "Activos"], + ["archived", "Archivados"], + ] as const + ).map(([v, label]) => ( + + ))} +
) : (
@@ -392,7 +439,14 @@ function TicketsContent() {
{ticket.title}
{ticket.requesterName || "N/A"} - + + + {ticket.archived && ( + + Archivado + + )} + {ticket.assigneeName || Sin asignar} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 471b775..c5b5dc2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -46,6 +46,7 @@ export async function getTickets(params?: { assigneeId?: number; requesterId?: number; groupId?: number; + archived?: boolean; search?: string; page?: number; pageSize?: number; @@ -109,6 +110,13 @@ export async function updateTicketStatus(id: number, status: string) { }); } +export async function setTicketArchived(id: number, archived: boolean) { + return fetchApi(`/api/helpdesk/ticket/${id}/archive`, { + method: "PUT", + body: JSON.stringify({ archived }), + }); +} + export async function assignTicket(id: number, assigneeId: number, groupId?: number) { return fetchApi(`/api/helpdesk/ticket/${id}/assign`, { method: "PUT", @@ -226,6 +234,8 @@ export interface Ticket { firstResponseAt?: string; resolvedAt?: string; closedAt?: string; + archived: boolean; + archivedAt?: string | null; } export interface TicketListResult {