feat: UI no-admin (vistas mis/asignados/sin-asignar, config solo admin)
This commit is contained in:
1 parent
3a1fee74e7
commit
a68999eca7
6 files changed
+124
-13
No files matched your search
@@ -8,9 +8,11 @@ import {
|
|||||||
deleteGroup,
|
deleteGroup,
|
||||||
type Group,
|
type Group,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
export default function DepartmentsPage() {
|
export default function DepartmentsPage() {
|
||||||
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -36,8 +38,13 @@ export default function DepartmentsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authLoading) return;
|
||||||
|
if (user?.role !== "admin") {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetchGroups();
|
fetchGroups();
|
||||||
}, [fetchGroups]);
|
}, [fetchGroups, authLoading, user?.role]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
@@ -94,6 +101,20 @@ export default function DepartmentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!authLoading && user?.role !== "admin") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-header">
|
||||||
|
<h1>Departamentos</h1>
|
||||||
|
</div>
|
||||||
|
<div className="alert alert-warning" role="alert">
|
||||||
|
<i className="bi bi-shield-lock me-2"></i>
|
||||||
|
Solo los administradores pueden gestionar departamentos.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
updateSla,
|
updateSla,
|
||||||
type SLAPolicy,
|
type SLAPolicy,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
export default function SlaPage() {
|
export default function SlaPage() {
|
||||||
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
const [policies, setPolicies] = useState<SLAPolicy[]>([]);
|
const [policies, setPolicies] = useState<SLAPolicy[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -37,8 +39,13 @@ export default function SlaPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authLoading) return;
|
||||||
|
if (user?.role !== "admin") {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetchPolicies();
|
fetchPolicies();
|
||||||
}, [fetchPolicies]);
|
}, [fetchPolicies, authLoading, user?.role]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
@@ -101,6 +108,20 @@ export default function SlaPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!authLoading && user?.role !== "admin") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-header">
|
||||||
|
<h1>Políticas SLA</h1>
|
||||||
|
</div>
|
||||||
|
<div className="alert alert-warning" role="alert">
|
||||||
|
<i className="bi bi-shield-lock me-2"></i>
|
||||||
|
Solo los administradores pueden gestionar las políticas SLA.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
type TeamMember,
|
type TeamMember,
|
||||||
type Group,
|
type Group,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
export default function TeamPage() {
|
export default function TeamPage() {
|
||||||
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
const [team, setTeam] = useState<TeamMember[]>([]);
|
const [team, setTeam] = useState<TeamMember[]>([]);
|
||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -41,8 +43,13 @@ export default function TeamPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authLoading) return;
|
||||||
|
if (user?.role !== "admin") {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}, [fetchData, authLoading, user?.role]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
@@ -97,6 +104,20 @@ export default function TeamPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!authLoading && user?.role !== "admin") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-header">
|
||||||
|
<h1>Equipo de Soporte</h1>
|
||||||
|
</div>
|
||||||
|
<div className="alert alert-warning" role="alert">
|
||||||
|
<i className="bi bi-shield-lock me-2"></i>
|
||||||
|
Solo los administradores pueden gestionar el equipo.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { getTickets, type Ticket } from "@/lib/api";
|
import { getTickets, type Ticket } from "@/lib/api";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
import BadgeStatus from "@/components/BadgeStatus";
|
import BadgeStatus from "@/components/BadgeStatus";
|
||||||
import BadgePriority from "@/components/BadgePriority";
|
import BadgePriority from "@/components/BadgePriority";
|
||||||
import PaginationComponent from "@/components/Pagination";
|
import PaginationComponent from "@/components/Pagination";
|
||||||
@@ -10,21 +11,33 @@ import Link from "next/link";
|
|||||||
|
|
||||||
export default function TicketsPage() {
|
export default function TicketsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { user } = useAuth();
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [view, setView] = useState<"all" | "mine" | "assigned" | "unassigned">("all");
|
||||||
const [statusFilter, setStatusFilter] = useState("");
|
const [statusFilter, setStatusFilter] = useState("");
|
||||||
const [priorityFilter, setPriorityFilter] = useState("");
|
const [priorityFilter, setPriorityFilter] = useState("");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const numericId = parseInt(user?.idUsuario ?? "", 10);
|
||||||
|
const hasNumericId = !isNaN(numericId);
|
||||||
|
|
||||||
const fetchTickets = useCallback(async () => {
|
const fetchTickets = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getTickets({
|
const data = await getTickets({
|
||||||
status: statusFilter || undefined,
|
status: statusFilter || undefined,
|
||||||
priority: priorityFilter || undefined,
|
priority: priorityFilter || undefined,
|
||||||
|
requesterId: view === "mine" && hasNumericId ? numericId : undefined,
|
||||||
|
assigneeId:
|
||||||
|
view === "assigned" && hasNumericId
|
||||||
|
? numericId
|
||||||
|
: view === "unassigned"
|
||||||
|
? 0
|
||||||
|
: undefined,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -37,7 +50,7 @@ export default function TicketsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [statusFilter, priorityFilter, search, page]);
|
}, [statusFilter, priorityFilter, search, page, view, numericId, hasNumericId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
@@ -56,6 +69,30 @@ export default function TicketsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* 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="filter-bar mb-4">
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<i className="bi bi-funnel text-muted"></i>
|
<i className="bi bi-funnel text-muted"></i>
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export default function Sidebar() {
|
|||||||
pathname === href || (href !== "/" && pathname.startsWith(href));
|
pathname === href || (href !== "/" && pathname.startsWith(href));
|
||||||
|
|
||||||
const fullName = user ? `${user.nombres} ${user.apPaterno}` : "Usuario";
|
const fullName = user ? `${user.nombres} ${user.apPaterno}` : "Usuario";
|
||||||
|
const isAdmin = user?.role === "admin";
|
||||||
|
const roleLabel =
|
||||||
|
user?.role === "admin" ? "Administrador" : user?.role === "agent" ? "Agente" : "Usuario";
|
||||||
const initials = user?.nombres
|
const initials = user?.nombres
|
||||||
?.split(" ")
|
?.split(" ")
|
||||||
.map((n: string) => n[0])
|
.map((n: string) => n[0])
|
||||||
@@ -54,13 +57,15 @@ export default function Sidebar() {
|
|||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<div
|
{isAdmin && (
|
||||||
className="text-uppercase mt-3 mb-1 px-3"
|
<>
|
||||||
style={{ fontSize: "0.7rem", letterSpacing: "0.08em", opacity: 0.6 }}
|
<div
|
||||||
>
|
className="text-uppercase mt-3 mb-1 px-3"
|
||||||
Configuración
|
style={{ fontSize: "0.7rem", letterSpacing: "0.08em", opacity: 0.6 }}
|
||||||
</div>
|
>
|
||||||
{configItems.map((item) => (
|
Configuración
|
||||||
|
</div>
|
||||||
|
{configItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
@@ -69,14 +74,16 @@ export default function Sidebar() {
|
|||||||
<i className={`bi ${item.icon}`}></i>
|
<i className={`bi ${item.icon}`}></i>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="sidebar-user">
|
<div className="sidebar-user">
|
||||||
<div className="avatar">{initials}</div>
|
<div className="avatar">{initials}</div>
|
||||||
<div className="sidebar-user-info">
|
<div className="sidebar-user-info">
|
||||||
<div className="name">{fullName}</div>
|
<div className="name">{fullName}</div>
|
||||||
<div className="role">{user?.tipo || "Usuario"}</div>
|
<div className="role">{roleLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function getTickets(params?: {
|
|||||||
status?: string;
|
status?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
assigneeId?: number;
|
assigneeId?: number;
|
||||||
|
requesterId?: number;
|
||||||
groupId?: number;
|
groupId?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -161,6 +162,9 @@ export interface HelpDeskUser {
|
|||||||
activo: number;
|
activo: number;
|
||||||
fono?: string;
|
fono?: string;
|
||||||
idSede?: number;
|
idSede?: number;
|
||||||
|
role?: string;
|
||||||
|
departmentId?: number;
|
||||||
|
departmentName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ticket {
|
export interface Ticket {
|
||||||
|
|||||||
Reference in new issue
Block a user