feat: botones Cambiar Estado y Asignar funcionales + departamento al crear ticket

This commit is contained in:
Nurfog committed 2026-09-14 13:09:53 -03:00
1 parent 5c36382600
commit 85fcabd5a5
2 files changed
+188 -27

No files matched your search

+155 -22
View File
@@ -8,36 +8,63 @@ import {
addComment, addComment,
updateTicketStatus, updateTicketStatus,
assignTicket, assignTicket,
getGroups,
getTeam,
type Ticket, type Ticket,
type Comment, type Comment,
type Group,
type TeamMember,
} from "@/lib/api"; } from "@/lib/api";
import BadgeStatus from "@/components/BadgeStatus"; import BadgeStatus from "@/components/BadgeStatus";
import BadgePriority from "@/components/BadgePriority"; import BadgePriority from "@/components/BadgePriority";
import Link from "next/link"; import Link from "next/link";
const STATUS_OPTIONS = [
{ value: "open", label: "Abierto" },
{ value: "pending", label: "Pendiente" },
{ value: "in_progress", label: "En Progreso" },
{ value: "resolved", label: "Resuelto" },
{ value: "closed", label: "Cerrado" },
];
export default function TicketDetailPage() { export default function TicketDetailPage() {
const params = useParams(); const params = useParams();
const id = Number(params.id); const id = Number(params.id);
const [ticket, setTicket] = useState<Ticket | null>(null); const [ticket, setTicket] = useState<Ticket | null>(null);
const [comments, setComments] = useState<Comment[]>([]); const [comments, setComments] = useState<Comment[]>([]);
const [groups, setGroups] = useState<Group[]>([]);
const [team, setTeam] = useState<TeamMember[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [isInternal, setIsInternal] = useState(false); const [isInternal, setIsInternal] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [acting, setActing] = useState(false);
const [error, setError] = useState("");
const [statusOpen, setStatusOpen] = useState(false);
const [assignOpen, setAssignOpen] = useState(false);
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
getTicket(id).catch(() => null), getTicket(id).catch(() => null),
getComments(id).catch(() => []), getComments(id).catch(() => []),
getGroups().catch(() => []),
getTeam().catch(() => []),
]) ])
.then(([ticketData, commentsData]) => { .then(([ticketData, commentsData, groupsData, teamData]) => {
setTicket(ticketData); setTicket(ticketData);
setComments(commentsData); setComments(commentsData);
setGroups(groupsData);
setTeam(teamData);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [id]); }, [id]);
const refreshTicket = async () => {
const updated = await getTicket(id).catch(() => null);
if (updated) setTicket(updated);
};
const handleAddComment = async (e: React.FormEvent) => { const handleAddComment = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!comment.trim()) return; if (!comment.trim()) return;
@@ -56,11 +83,50 @@ export default function TicketDetailPage() {
}; };
const handleStatusChange = async (newStatus: string) => { const handleStatusChange = async (newStatus: string) => {
setStatusOpen(false);
setError("");
setActing(true);
try { try {
await updateTicketStatus(id, newStatus); await updateTicketStatus(id, newStatus);
setTicket((prev) => (prev ? { ...prev, status: newStatus } : prev)); await refreshTicket();
} catch { } catch (err: any) {
// ignore setError(err.message || "No se pudo cambiar el estado");
} finally {
setActing(false);
}
};
const handleAssignDepartment = async (groupId: number) => {
if (!ticket) return;
setAssignOpen(false);
setError("");
setActing(true);
try {
await assignTicket(id, ticket.assigneeId ?? 0, groupId);
await refreshTicket();
} catch (err: any) {
setError(err.message || "No se pudo asignar el departamento");
} finally {
setActing(false);
}
};
const handleAssignAgent = async (idUsuario: string | null) => {
if (!ticket) return;
setAssignOpen(false);
setError("");
setActing(true);
try {
await assignTicket(
id,
idUsuario === null ? 0 : Number(idUsuario),
ticket.groupId ?? undefined
);
await refreshTicket();
} catch (err: any) {
setError(err.message || "No se pudo asignar el agente");
} finally {
setActing(false);
} }
}; };
@@ -100,44 +166,111 @@ export default function TicketDetailPage() {
</div> </div>
</div> </div>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
{/* Status Change */} {/* Status Change (dropdown controlado por React: sin bootstrap.js) */}
<div className="dropdown"> <div className="position-relative">
<button className="btn btn-outline-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown"> <button
className="btn btn-outline-primary btn-sm"
disabled={acting}
onClick={() => {
setStatusOpen((o) => !o);
setAssignOpen(false);
}}
>
<i className="bi bi-arrow-repeat me-1"></i>Cambiar Estado <i className="bi bi-arrow-repeat me-1"></i>Cambiar Estado
</button> </button>
<ul className="dropdown-menu"> {statusOpen && (
{[ <>
{ value: "Open", label: "Abierto" }, <div
{ value: "Pending", label: "Pendiente" }, className="position-fixed top-0 start-0 w-100 h-100"
{ value: "InProgress", label: "En Progreso" }, style={{ zIndex: 1000 }}
{ value: "Resolved", label: "Resuelto" }, onClick={() => setStatusOpen(false)}
{ value: "Closed", label: "Cerrado" }, />
].map((s) => ( <ul className="dropdown-menu show position-absolute end-0" style={{ zIndex: 1001 }}>
{STATUS_OPTIONS.map((s) => (
<li key={s.value}> <li key={s.value}>
<button className="dropdown-item" onClick={() => handleStatusChange(s.value)}> <button
className={`dropdown-item ${ticket.status === s.value ? "active" : ""}`}
onClick={() => handleStatusChange(s.value)}
>
{s.label} {s.label}
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</>
)}
</div> </div>
{/* Assign */} {/* Assign (dropdown controlado por React: sin bootstrap.js) */}
<div className="dropdown"> <div className="position-relative">
<button className="btn btn-outline-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown"> <button
className="btn btn-outline-primary btn-sm"
disabled={acting}
onClick={() => {
setAssignOpen((o) => !o);
setStatusOpen(false);
}}
>
<i className="bi bi-person-plus me-1"></i>Asignar <i className="bi bi-person-plus me-1"></i>Asignar
</button> </button>
<ul className="dropdown-menu"> {assignOpen && (
<>
<div
className="position-fixed top-0 start-0 w-100 h-100"
style={{ zIndex: 1000 }}
onClick={() => setAssignOpen(false)}
/>
<ul className="dropdown-menu show position-absolute end-0" style={{ zIndex: 1001, minWidth: "220px" }}>
<li> <li>
<button className="dropdown-item" onClick={() => assignTicket(id, 0)}> <h6 className="dropdown-header">Departamento</h6>
Sin asignar </li>
{groups.length === 0 && (
<li><span className="dropdown-item-text text-muted">Sin departamentos</span></li>
)}
{groups.map((g) => (
<li key={g.id}>
<button
className={`dropdown-item ${ticket.groupId === g.id ? "active" : ""}`}
onClick={() => handleAssignDepartment(g.id)}
>
<i className="bi bi-diagram-3 me-2"></i>{g.name}
</button> </button>
</li> </li>
))}
<li><hr className="dropdown-divider" /></li>
<li>
<h6 className="dropdown-header">Agente</h6>
</li>
<li>
<button className="dropdown-item" onClick={() => handleAssignAgent(null)}>
<i className="bi bi-person-dash me-2"></i>Sin asignar
</button>
</li>
{team.map((m) => (
<li key={m.idUsuario}>
<button
className="dropdown-item"
onClick={() => handleAssignAgent(m.idUsuario)}
>
<i className="bi bi-person me-2"></i>
{m.nombres} {m.apPaterno}
</button>
</li>
))}
</ul> </ul>
</>
)}
</div> </div>
</div> </div>
</div> </div>
{error && (
<div className="alert alert-danger py-2" role="alert">
<i className="bi bi-exclamation-circle me-2"></i>
{error}
</div>
)}
<div className="row g-4"> <div className="row g-4">
{/* Comments Timeline */} {/* Comments Timeline */}
<div className="col-lg-8"> <div className="col-lg-8">
+33 -5
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { createTicket } from "@/lib/api"; import { createTicket, getGroups, type Group } from "@/lib/api";
import Link from "next/link"; import Link from "next/link";
export default function NewTicketPage() { export default function NewTicketPage() {
@@ -13,13 +13,23 @@ 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 [priority, setPriority] = useState("Medium");
const [groups, setGroups] = useState<Group[]>([]);
const [groupId, setGroupId] = useState("");
useEffect(() => {
getGroups().then(setGroups).catch(() => []);
}, []);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
if (!groupId) {
setError("Debes seleccionar un departamento");
return;
}
setLoading(true); setLoading(true);
try { try {
const result = await createTicket({ title, description, priority }); const result = await createTicket({ title, description, priority, groupId: Number(groupId) });
router.push(`/tickets/${result.id}`); router.push(`/tickets/${result.id}`);
} catch (err: any) { } catch (err: any) {
setError(err.message || "Error al crear el ticket"); setError(err.message || "Error al crear el ticket");
@@ -76,11 +86,28 @@ export default function NewTicketPage() {
></textarea> ></textarea>
</div> </div>
<div className="mb-4"> <div className="row">
<div className="col-md-6 mb-4">
<label className="form-label fw-semibold">Departamento</label>
<select
className="form-select"
value={groupId}
onChange={(e) => setGroupId(e.target.value)}
required
>
<option value="">Seleccionar departamento...</option>
{groups.map((g) => (
<option key={g.id} value={g.id}>
{g.name}
</option>
))}
</select>
</div>
<div className="col-md-6 mb-4">
<label className="form-label fw-semibold">Prioridad</label> <label className="form-label fw-semibold">Prioridad</label>
<select <select
className="form-select" className="form-select"
style={{ maxWidth: "200px" }}
value={priority} value={priority}
onChange={(e) => setPriority(e.target.value)} onChange={(e) => setPriority(e.target.value)}
> >
@@ -90,6 +117,7 @@ export default function NewTicketPage() {
<option value="Critical">Crítica</option> <option value="Critical">Crítica</option>
</select> </select>
</div> </div>
</div>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<button <button