diff --git a/client/src/containers/Admin/Tickets/TicketsCreated.css b/client/src/containers/Admin/Tickets/TicketsCreated.css index cdd988c..47a47ed 100644 --- a/client/src/containers/Admin/Tickets/TicketsCreated.css +++ b/client/src/containers/Admin/Tickets/TicketsCreated.css @@ -1,83 +1,78 @@ -/* .tickets-header { - text-align: center; - margin-bottom: 20px; -} - -.ticket-section { - margin-bottom: 40px; -} - -h1 { - color: #333; +/* Wrapper */ +.tickets-wrapper { + padding: 20px; + transition: 0.3s ease; } -table { - width: 100%; - border-collapse: collapse; - margin-top: 10px; +/* Dark mode */ +.tickets-wrapper.dark { + background: #1e1e1e; + color: white; } -th, -td { - padding: 10px; - text-align: left; - border: 1px solid #ccc; +.tickets-wrapper.dark table { + color: white; } -th { - background-color: #f4f4f4; +.tickets-wrapper.dark th { + background: #333; } -td { - background-color: #fff; +.tickets-wrapper.dark td { + background: #222; } -td a { - color: #007bff; - text-decoration: none; +/* Header */ +.header-row { + display: flex; + justify-content: space-between; + align-items: center; } -td a:hover { - text-decoration: underline; +/* Dark mode button */ +.dark-toggle { + padding: 8px 12px; + border-radius: 6px; + border: none; + cursor: pointer; } -.link-home { - display: block; - margin-top: 20px; - text-align: center; - color: #007bff; -} */ - -.tickets-wrapper { - padding: 20px; +/* Filters */ +.filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin: 20px 0; } -.tickets-title { - text-align: center; - margin-bottom: 20px; - color: #333; +.filters input, +.filters select { + padding: 8px; + border-radius: 6px; + border: 1px solid #ccc; } -/* Search Bar */ -.search-container { - text-align: center; - margin-bottom: 20px; +/* Export Button */ +.export-btn { + background: #007bff; + color: white; + border: none; + padding: 8px 14px; + border-radius: 6px; + cursor: pointer; } -.search-container input { - width: 90%; - max-width: 400px; - padding: 10px 12px; - border-radius: 8px; - border: 1px solid #ccc; +.export-btn:hover { + opacity: 0.85; } -/* Table */ -.table-container { +/* Table Scroll Wrapper */ +.table-scroll { width: 100%; overflow-x: auto; } +/* Table */ .tickets-table { width: 100%; border-collapse: collapse; @@ -89,29 +84,57 @@ td a:hover { border: 1px solid #ddd; } -.tickets-table th { - background-color: #f8f8f8; - text-align: left; +.wrap { + word-wrap: break-word; + max-width: 250px; +} + +/* Status Badges */ +.badge { + padding: 5px 8px; + border-radius: 6px; + font-weight: bold; + text-transform: capitalize; +} + +.badge.open { + background: #4caf50; + color: white; +} + +.badge.closed { + background: #616161; + color: white; +} + +.badge.in-progress { + background: #ff9800; + color: white; +} + +.badge.archived { + background: #b71c1c; + color: white; } +/* Archive Button */ .archive-btn { - background: #c62828; + background: #d32f2f; color: white; border: none; padding: 6px 10px; - border-radius: 5px; + border-radius: 6px; cursor: pointer; } .archive-btn:hover { - background: #b71c1c; + opacity: 0.85; } /* Pagination */ .pagination { display: flex; justify-content: center; - align-items: center; margin-top: 20px; gap: 15px; } @@ -127,34 +150,24 @@ td a:hover { .pagination button:disabled { opacity: 0.5; - cursor: default; } -/* Home link */ +/* Home Link */ .home-link { display: block; - margin-top: 30px; text-align: center; + margin-top: 25px; color: #007bff; text-decoration: none; } -.home-link:hover { - text-decoration: underline; -} - -/* Mobile Responsive */ +/* MOBILE */ @media (max-width: 600px) { - .tickets-table th, - .tickets-table td { - padding: 6px; - } - - .search-container input { - width: 95%; + .filters { + flex-direction: column; } - .pagination span { - font-size: 14px; + .wrap { + max-width: 140px; } } diff --git a/client/src/containers/Admin/Tickets/TicketsCreated.tsx b/client/src/containers/Admin/Tickets/TicketsCreated.tsx index 100a74a..84e54dd 100644 --- a/client/src/containers/Admin/Tickets/TicketsCreated.tsx +++ b/client/src/containers/Admin/Tickets/TicketsCreated.tsx @@ -85,6 +85,168 @@ // export default TicketsCreated; +// import React, { useEffect, useState } from "react"; +// import axios from "axios"; +// import { Link } from "react-router-dom"; +// import "./TicketsCreated.css"; + +// interface Ticket { +// _id: string; +// name?: string; +// subject?: string; +// description?: string; +// createdAt?: string; +// } + +// const ITEMS_PER_PAGE = 10; + +// const TicketsCreated: React.FC = () => { +// const [itTickets, setItTickets] = useState([]); +// const [userTickets, setUserTickets] = useState([]); +// const [search, setSearch] = useState(""); +// const [filtered, setFiltered] = useState([]); +// const [page, setPage] = useState(1); + +// // Fetch IT tickets +// const fetchItTickets = async () => { +// try { +// const { data } = await axios.get(`/api/it-help/view`); +// setItTickets(data); +// } catch (error) { +// console.error("Error fetching IT tickets:", error); +// } +// }; + +// // Fetch User tickets +// const fetchUserTickets = async () => { +// try { +// const { data } = await axios.get(`/api/employee-help/view`); +// setUserTickets(data); +// } catch (error) { +// console.error("Error fetching user tickets:", error); +// } +// }; + +// useEffect(() => { +// fetchItTickets(); +// fetchUserTickets(); +// }, []); + +// // Combine both ticket sources +// useEffect(() => { +// const combined = [...itTickets, ...userTickets]; + +// const cleanSearch = search.toLowerCase(); +// const filteredTickets = combined.filter((t) => { +// return ( +// (t.name?.toLowerCase().includes(cleanSearch) ?? false) || +// (t.subject?.toLowerCase().includes(cleanSearch) ?? false) || +// (t.description?.toLowerCase().includes(cleanSearch) ?? false) +// ); +// }); + +// setFiltered(filteredTickets); +// setPage(1); // reset to first page on search +// }, [search, itTickets, userTickets]); + +// const startIndex = (page - 1) * ITEMS_PER_PAGE; +// const pageTickets = filtered.slice(startIndex, startIndex + ITEMS_PER_PAGE); +// const pages = Math.ceil(filtered.length / ITEMS_PER_PAGE); + +// const handleArchive = async (id: string) => { +// try { +// await axios.put(`/api/tickets/archive/${id}`); +// setItTickets(itTickets.filter((t) => t._id !== id)); +// setUserTickets(userTickets.filter((t) => t._id !== id)); +// } catch (error) { +// console.error("Error archiving ticket:", error); +// } +// }; + +// return ( +//
+//

All Tickets

+ +// {/* Search Bar */} +//
+// setSearch(e.target.value)} +// /> +//
+ +//
+// +// +// +// +// +// +// +// +// +// +// +// +// {pageTickets.length > 0 ? ( +// pageTickets.map((ticket) => ( +// +// +// +// +// +// +// +// +// )) +// ) : ( +// +// +// +// )} +// +//
ViewNameSubjectDescriptionCreatedArchive
+// Open +// {ticket.name ?? "—"}{ticket.subject ?? "—"}{ticket.description ?? "—"}{ticket.createdAt ? new Date(ticket.createdAt).toLocaleString() : "—"} +// +//
+// No tickets found +//
+//
+ +// {/* Pagination */} +// {pages > 1 && ( +//
+// + +// +// Page {page} of {pages} +// + +// +//
+// )} + +// +// Back Home +// +//
+// ); +// }; + +// export default TicketsCreated; + import React, { useEffect, useState } from "react"; import axios from "axios"; import { Link } from "react-router-dom"; @@ -96,88 +258,163 @@ interface Ticket { subject?: string; description?: string; createdAt?: string; + status?: "open" | "closed" | "archived" | "in-progress"; + type?: "it" | "user"; } const ITEMS_PER_PAGE = 10; const TicketsCreated: React.FC = () => { - const [itTickets, setItTickets] = useState([]); - const [userTickets, setUserTickets] = useState([]); + const [tickets, setTickets] = useState([]); const [search, setSearch] = useState(""); - const [filtered, setFiltered] = useState([]); + const [statusFilter, setStatusFilter] = useState("all"); + const [typeFilter, setTypeFilter] = useState("all"); + const [sortBy, setSortBy] = useState(""); const [page, setPage] = useState(1); + const [darkMode, setDarkMode] = useState(false); - // Fetch IT tickets - const fetchItTickets = async () => { + const fetchTickets = async () => { try { - const { data } = await axios.get(`/api/it-help/view`); - setItTickets(data); + const itRes = await axios.get(`/api/it-help/view`); + const userRes = await axios.get(`/api/employee-help/view`); + + // Label sources BEFORE merge + const it = itRes.data.map((t: Ticket) => ({ ...t, type: "it" })); + const user = userRes.data.map((t: Ticket) => ({ ...t, type: "user" })); + + setTickets([...it, ...user]); } catch (error) { - console.error("Error fetching IT tickets:", error); + console.error("Error loading tickets:", error); } }; - // Fetch User tickets - const fetchUserTickets = async () => { + useEffect(() => { + fetchTickets(); + }, []); + + const handleArchive = async (id: string) => { try { - const { data } = await axios.get(`/api/employee-help/view`); - setUserTickets(data); + await axios.put(`/api/tickets/archive/${id}`); + setTickets((prev) => prev.filter((t) => t._id !== id)); } catch (error) { - console.error("Error fetching user tickets:", error); + console.error("Error archiving:", error); } }; - useEffect(() => { - fetchItTickets(); - fetchUserTickets(); - }, []); + // ---- FILTERING ---- + const filteredTickets = tickets.filter((ticket) => { + const matchesSearch = + ticket.name?.toLowerCase().includes(search.toLowerCase()) || + ticket.subject?.toLowerCase().includes(search.toLowerCase()) || + ticket.description?.toLowerCase().includes(search.toLowerCase()); - // Combine both ticket sources - useEffect(() => { - const combined = [...itTickets, ...userTickets]; + const matchesStatus = + statusFilter === "all" || ticket.status === statusFilter; + + const matchesType = typeFilter === "all" || ticket.type === typeFilter; + + return matchesSearch && matchesStatus && matchesType; + }); + + // ---- SORTING ---- + const sortedTickets = [...filteredTickets].sort((a, b) => { + if (!sortBy) return 0; - const cleanSearch = search.toLowerCase(); - const filteredTickets = combined.filter((t) => { + if (sortBy === "createdAt") { return ( - (t.name?.toLowerCase().includes(cleanSearch) ?? false) || - (t.subject?.toLowerCase().includes(cleanSearch) ?? false) || - (t.description?.toLowerCase().includes(cleanSearch) ?? false) + new Date(b.createdAt || "").getTime() - + new Date(a.createdAt || "").getTime() ); - }); + } - setFiltered(filteredTickets); - setPage(1); // reset to first page on search - }, [search, itTickets, userTickets]); + return ((a as any)[sortBy] || "") + .toString() + .localeCompare(((b as any)[sortBy] || "").toString()); + }); + // ---- PAGINATION ---- const startIndex = (page - 1) * ITEMS_PER_PAGE; - const pageTickets = filtered.slice(startIndex, startIndex + ITEMS_PER_PAGE); - const pages = Math.ceil(filtered.length / ITEMS_PER_PAGE); + const pageTickets = sortedTickets.slice(startIndex, startIndex + ITEMS_PER_PAGE); + const totalPages = Math.ceil(sortedTickets.length / ITEMS_PER_PAGE); - const handleArchive = async (id: string) => { - try { - await axios.put(`/api/tickets/archive/${id}`); - setItTickets(itTickets.filter((t) => t._id !== id)); - setUserTickets(userTickets.filter((t) => t._id !== id)); - } catch (error) { - console.error("Error archiving ticket:", error); - } + const exportCSV = () => { + const rows = [ + ["Name", "Subject", "Description", "Created At", "Status", "Type"], + ...tickets.map((t) => [ + t.name, + t.subject, + t.description, + t.createdAt, + t.status, + t.type, + ]), + ]; + + const csvContent = + "data:text/csv;charset=utf-8," + + rows.map((row) => row.join(",")).join("\n"); + + const link = document.createElement("a"); + link.href = encodeURI(csvContent); + link.download = "tickets_export.csv"; + link.click(); }; return ( -
-

All Tickets

+
+
+

All Tickets

- {/* Search Bar */} -
+ {/* Dark Mode Toggle */} + +
+ + {/* Search / Filters */} +
setSearch(e.target.value)} /> + + + + + + + +
-
+ {/* Scrollable Table */} +
@@ -186,33 +423,44 @@ const TicketsCreated: React.FC = () => { + + - {pageTickets.length > 0 ? ( - pageTickets.map((ticket) => ( - - - - - - - - - )) - ) : ( + {pageTickets.map((t) => ( + + + + + + + + + + ))} + + {pageTickets.length === 0 && ( - @@ -222,17 +470,20 @@ const TicketsCreated: React.FC = () => { {/* Pagination */} - {pages > 1 && ( + {totalPages > 1 && (
- Page {page} of {pages} + Page {page} of {totalPages} -
Subject Description CreatedStatus Archive
- Open - {ticket.name ?? "—"}{ticket.subject ?? "—"}{ticket.description ?? "—"}{ticket.createdAt ? new Date(ticket.createdAt).toLocaleString() : "—"} - -
+ Open + {t.name || "—"}{t.subject || "—"}{t.description || "—"} + {t.createdAt + ? new Date(t.createdAt).toLocaleString() + : "—"} + + + {t.status || "open"} + + + +
+ No tickets found