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,
updateTicketStatus,
assignTicket,
getGroups,
getTeam,
type Ticket,
type Comment,
type Group,
type TeamMember,
} from "@/lib/api";
import BadgeStatus from "@/components/BadgeStatus";
import BadgePriority from "@/components/BadgePriority";
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() {
const params = useParams();
const id = Number(params.id);
const [ticket, setTicket] = useState<Ticket | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [groups, setGroups] = useState<Group[]>([]);
const [team, setTeam] = useState<TeamMember[]>([]);
const [loading, setLoading] = useState(true);
const [comment, setComment] = useState("");
const [isInternal, setIsInternal] = 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(() => {
Promise.all([
getTicket(id).catch(() => null),
getComments(id).catch(() => []),
getGroups().catch(() => []),
getTeam().catch(() => []),
])
.then(([ticketData, commentsData]) => {
.then(([ticketData, commentsData, groupsData, teamData]) => {
setTicket(ticketData);
setComments(commentsData);
setGroups(groupsData);
setTeam(teamData);
})
.finally(() => setLoading(false));
}, [id]);
const refreshTicket = async () => {
const updated = await getTicket(id).catch(() => null);
if (updated) setTicket(updated);
};
const handleAddComment = async (e: React.FormEvent) => {
e.preventDefault();
if (!comment.trim()) return;
@@ -56,11 +83,50 @@ export default function TicketDetailPage() {
};
const handleStatusChange = async (newStatus: string) => {
setStatusOpen(false);
setError("");
setActing(true);
try {
await updateTicketStatus(id, newStatus);
setTicket((prev) => (prev ? { ...prev, status: newStatus } : prev));
} catch {
// ignore
await refreshTicket();
} catch (err: any) {
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 className="d-flex gap-2">
{/* Status Change */}
<div className="dropdown">
<button className="btn btn-outline-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
{/* Status Change (dropdown controlado por React: sin bootstrap.js) */}
<div className="position-relative">
<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
</button>
<ul className="dropdown-menu">
{[
{ value: "Open", label: "Abierto" },
{ value: "Pending", label: "Pendiente" },
{ value: "InProgress", label: "En Progreso" },
{ value: "Resolved", label: "Resuelto" },
{ value: "Closed", label: "Cerrado" },
].map((s) => (
{statusOpen && (
<>
<div
className="position-fixed top-0 start-0 w-100 h-100"
style={{ zIndex: 1000 }}
onClick={() => setStatusOpen(false)}
/>
<ul className="dropdown-menu show position-absolute end-0" style={{ zIndex: 1001 }}>
{STATUS_OPTIONS.map((s) => (
<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}
</button>
</li>
))}
</ul>
</>
)}
</div>
{/* Assign */}
<div className="dropdown">
<button className="btn btn-outline-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
{/* Assign (dropdown controlado por React: sin bootstrap.js) */}
<div className="position-relative">
<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
</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>
<button className="dropdown-item" onClick={() => assignTicket(id, 0)}>
Sin asignar
<h6 className="dropdown-header">Departamento</h6>
</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>
</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>
</>
)}
</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">
{/* Comments Timeline */}
<div className="col-lg-8">
+33 -5
View File
@@ -1,8 +1,8 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { createTicket } from "@/lib/api";
import { createTicket, getGroups, type Group } from "@/lib/api";
import Link from "next/link";
export default function NewTicketPage() {
@@ -13,13 +13,23 @@ export default function NewTicketPage() {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
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) => {
e.preventDefault();
setError("");
if (!groupId) {
setError("Debes seleccionar un departamento");
return;
}
setLoading(true);
try {
const result = await createTicket({ title, description, priority });
const result = await createTicket({ title, description, priority, groupId: Number(groupId) });
router.push(`/tickets/${result.id}`);
} catch (err: any) {
setError(err.message || "Error al crear el ticket");
@@ -76,11 +86,28 @@ export default function NewTicketPage() {
></textarea>
</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>
<select
className="form-select"
style={{ maxWidth: "200px" }}
value={priority}
onChange={(e) => setPriority(e.target.value)}
>
@@ -90,6 +117,7 @@ export default function NewTicketPage() {
<option value="Critical">Crítica</option>
</select>
</div>
</div>
<div className="d-flex gap-2">
<button