diff --git a/app/[locale]/(protected)/supervisor/ticketing/components/table.tsx b/app/[locale]/(protected)/supervisor/ticketing/components/table.tsx index 31bdd403..14969f62 100644 --- a/app/[locale]/(protected)/supervisor/ticketing/components/table.tsx +++ b/app/[locale]/(protected)/supervisor/ticketing/components/table.tsx @@ -1,238 +1,859 @@ "use client"; import * as React from "react"; -import { - ColumnDef, - ColumnFiltersState, - PaginationState, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { ticketingPagination } from "@/service/ticketing/ticketing"; import { Button } from "@/components/ui/button"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - ChevronLeft, - ChevronRight, - Eye, - MoreVertical, - Search, - SquarePen, - Trash2, - TrendingDown, - TrendingUp, -} from "lucide-react"; -import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { ChevronLeft, ChevronRight, Search, MoreVertical } from "lucide-react"; import { DropdownMenu, + DropdownMenuTrigger, DropdownMenuContent, - DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, - DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { InputGroup, InputGroupText } from "@/components/ui/input-group"; -import { paginationBlog } from "@/service/blog/blog"; -import { ticketingPagination } from "@/service/ticketing/ticketing"; -import { Badge } from "@/components/ui/badge"; -import { useRouter, useSearchParams } from "next/navigation"; -import TablePagination from "@/components/table/table-pagination"; -import columns from "./columns"; +import { cn } from "@/lib/utils"; +import FormDetailTicketing from "@/components/form/ticketing/ticketing-detail-form"; -const TicketingTable = () => { - const router = useRouter(); - const searchParams = useSearchParams(); +/** + * TicketingLayout + * + * Features implemented: + * - Header with total ticket count + * - Collapsible left sidebar with menu and a toggle arrow (shows total content count) + * - Middle issue list (list items, select all checkbox, search input with debounce, sort latest/oldest) + * - Right chat panel (bubble style) that opens when clicking an issue + * - Chat input area with Translate, Kirim & Resolve, Kirim buttons + * + * Notes: + * - Replace / adjust icons and utilities if your project differs. + * - ticketingPagination(search, pageSize, pageIndex) is used; ensure it returns the same structure as before. + */ - const [dataTable, setDataTable] = React.useState([]); - const [totalData, setTotalData] = React.useState(1); - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] +type Issue = { + id: string | number; + title?: string; + source?: string; // e.g., "instagram", "facebook", "tiktok", "youtube", "comment" + createdAt?: string; + status?: string; + timeAgo?: string; + // any other fields from API +}; + +export default function TicketingLayout() { + // data & pagination + const [issues, setIssues] = React.useState([]); + const [totalElements, setTotalElements] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(1); + + // controls + const [isSidebarOpen, setIsSidebarOpen] = React.useState(true); + const [selectedIssue, setSelectedIssue] = React.useState(null); + const [search, setSearch] = React.useState(""); + const [sortOrder, setSortOrder] = React.useState<"latest" | "oldest">( + "latest" + ); + const [page, setPage] = React.useState(1); + const [pageSize, setPageSize] = React.useState(10); + const [selectedTicketId, setSelectedTicketId] = React.useState( + null ); - const [search, setSearch] = React.useState(""); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [showData, setShowData] = React.useState("10"); + // selection + const [selectedMap, setSelectedMap] = React.useState>( + {} + ); + const allSelected = React.useMemo(() => { + if (!issues.length) return false; + return issues.every((i) => selectedMap[String(i.id)]); + }, [issues, selectedMap]); - const [rowSelection, setRowSelection] = React.useState({}); - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: Number(showData), - }); - const [page, setPage] = React.useState(1); - const [totalPage, setTotalPage] = React.useState(1); - - const table = useReactTable({ - data: dataTable, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - onPaginationChange: setPagination, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - pagination, - }, - }); - - let typingTimer: any; - const doneTypingInterval = 1500; - - const handleKeyUp = () => { - clearTimeout(typingTimer); - typingTimer = setTimeout(doneTyping, doneTypingInterval); - }; - - const handleKeyDown = () => { - clearTimeout(typingTimer); - }; - - async function doneTyping() { - fetchData(); - } - - React.useEffect(() => { - const pageFromUrl = searchParams?.get("page"); - if (pageFromUrl) { - setPage(Number(pageFromUrl)); - } - }, [searchParams]); + // search debounce + React.useEffect(() => { + const t = setTimeout(() => { + fetchData(); + }, 450); + return () => clearTimeout(t); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [search, page, pageSize, sortOrder]); React.useEffect(() => { + // initial fetch fetchData(); - }, [page]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); async function fetchData() { try { - const res = await ticketingPagination(search, Number(showData), page - 1); + // note: existing API used earlier: ticketingPagination(search, Number(showData), page - 1) + const res = await ticketingPagination(search, pageSize, page - 1); const data = res?.data?.data; - const contentData = data?.content; - contentData.forEach((item: any, index: number) => { - item.no = (page - 1) * Number(showData) + index + 1; + const content = data?.content || []; + // map fields to Issue type (adjust if necessary) + const mapped: Issue[] = content.map((it: any, idx: number) => ({ + id: it.id ?? idx, + title: it.title ?? it.subject ?? "No Title", + source: it.source ?? it.channel ?? "unknown", + timeAgo: it.timeAgo ?? it.duration ?? "—", + createdAt: it.createdAt, + status: it.status, + })); + setIssues(mapped); + setTotalElements(data?.totalElements ?? 0); + setTotalPages(data?.totalPages ?? 1); + + // keep selection map bounded to current items + const newMap: Record = {}; + mapped.forEach((m) => { + newMap[String(m.id)] = Boolean(selectedMap[String(m.id)]); }); - - console.log("contentData : ", contentData); - - setDataTable(contentData); - setTotalData(data?.totalElements); - setTotalPage(data?.totalPages); - } catch (error) { - console.error("Error fetching tasks:", error); + setSelectedMap(newMap); + } catch (err) { + console.error("fetchData error", err); } } + // select all toggle + function toggleSelectAll() { + const next: Record = {}; + if (!allSelected) { + issues.forEach((i) => { + next[String(i.id)] = true; + }); + } else { + // clear + } + setSelectedMap(next); + } + + function toggleSelectOne(id: string | number) { + const key = String(id); + setSelectedMap((prev) => ({ ...prev, [key]: !prev[key] })); + } + + // UI helpers + function sourceToIconLabel(source?: string) { + if (!source) return { label: "Other", short: "OT" }; + const s = source.toLowerCase(); + if (s.includes("insta") || s.includes("instagram")) + return { label: "Instagram", short: "IG" }; + if (s.includes("facebook") || s.includes("fb")) + return { label: "Facebook", short: "FB" }; + if (s.includes("tiktok")) return { label: "TikTok", short: "TT" }; + if (s.includes("youtube")) return { label: "YouTube", short: "YT" }; + if (s.includes("comment") || s.includes("kolom")) + return { label: "Komentar", short: "CM" }; + return { label: source, short: source.slice(0, 2).toUpperCase() }; + } + + // Chat actions + const [chatMessages, setChatMessages] = React.useState< + { id: string; from: "user" | "agent"; text: string; time?: string }[] + >([]); + const [replyText, setReplyText] = React.useState(""); + + React.useEffect(() => { + // clear chatMessages when selecting new issue and optionally load messages for issue + if (!selectedIssue) { + setChatMessages([]); + return; + } + // For demo: populate sample messages (replace by real API call) + setChatMessages([ + { + id: "m1", + from: "user", + text: "Hallo, untuk patroli hari ini sudah bisa di akses di daily tasks, silahkan anda periksa", + time: "06:00", + }, + { + id: "m2", + from: "agent", + text: "Terima kasih. Kami akan cek dan tindak lanjut segera. Mohon lampirkan bukti bila ada.", + time: "06:05", + }, + ]); + }, [selectedIssue]); + + function sendReply(closeAfter = false) { + if (!replyText.trim()) return; + const id = `m${Date.now()}`; + setChatMessages((c) => [ + ...c, + { + id, + from: "agent", + text: replyText, + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + }, + ]); + setReplyText(""); + if (closeAfter) { + // emulate resolve + close bubble + setTimeout(() => { + setSelectedIssue(null); + }, 300); + } + } + + function handleTranslate() { + // placeholder: you can call your translate API here + if (!replyText.trim()) return; + // naive "translation" demo: append "(translated)" + setReplyText((t) => t + " (translated)"); + } + return ( - <> -
- setSearch(e.target.value)} - className="max-w-[300px]" - /> -
- - - - - - - - 1 - 10 Data - - - 1 - 20 Data - - - 1 - 25 Data - - - 1 - 50 Data - - - - +
+ {/* Header */} +
+
+

+ Semua Ticket: {totalElements} +

- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
- - - ); -}; -export default TicketingTable; +
+ {/* Sidebar */} + + + {/* Middle: Issue List */} +
+ {/* top controls: select all, search, sort */} +
+
+ +
+
+ setSearch(e.target.value)} + className="pl-10" + /> +
+ +
+
+
+ +
+ + + + + + + setSortOrder(value as "latest" | "oldest") + } + > + + Latest + + + Oldest + + + + +
+ +
+ + + + + +
+ Show +
+ { + setPageSize(Number(v)); + setPage(1); + }} + > + + 10 + + + 20 + + + 25 + + + 50 + + +
+
+
+
+
+ + {/* list */} +
+
    + {issues.length === 0 ? ( +
  • + No issues +
  • + ) : ( + issues.map((it) => { + const key = String(it.id); + const src = sourceToIconLabel(it.source); + return ( +
  • { + setSelectedIssue(it); + setSelectedTicketId(String(it.id)); // ✅ ini yang bikin detail muncul + }} + className={cn( + "flex items-start gap-3 px-4 py-3 border-b cursor-pointer hover:bg-gray-50", + selectedIssue?.id === it.id ? "bg-gray-100" : "" + )} + > +
    + { + e.stopPropagation(); + toggleSelectOne(it.id); + }} + onClick={(e) => e.stopPropagation()} + className="h-4 w-4 mt-1" + /> +
    + +
    +
    +
    + + {src.short} + +
    +
    + {(it.title ?? "") + .split(" ") + .slice(0, 25) + .join(" ") + + ((it.title ?? "").split(" ").length > 25 + ? "..." + : "")} +
    + +
    + {src.label} + + {it.timeAgo ?? "—"} +
    +
    +
    + +
    + New Issue +
    + {it.timeAgo} +
    +
    +
    +
    +
  • + ); + }) + )} +
+
+ + {/* pagination simple */} +
+
+ {`1-${pageSize} of ${totalElements}`} +
+
+ +
+ {page} / {totalPages} +
+ +
+
+
+ + {/* Right: Chat Panel */} +
+ {selectedTicketId ? ( + + ) : ( +
+ Pilih ticket dari sebelah kiri +
+ )} +
+ {/*
+
+
+
+ {selectedIssue ? ( +
+
+ {(selectedIssue?.title ?? "") + .split(" ") + .slice(0, 25) + .join(" ") + + ((selectedIssue?.title ?? "").split(" ").length > 25 + ? "..." + : "")} +
+ +
+ • {selectedIssue.source} +
+
+ ) : ( +
Chat
+ )} +
+
+
+ +
+ {!selectedIssue ? ( +
+
+
+ + + +
+
+ Pilih issue untuk melihat detail +
+
+
+ ) : ( +
+ {chatMessages.map((m) => ( +
+
+ {m.text} +
+
+ {m.from === "agent" ? "Anda" : "User"} + + {m.time} +
+
+ ))} +
+ )} +
+ +
+
+
+