mediahub-fe/app/[locale]/(protected)/supervisor/ticketing/components/table.tsx

854 lines
30 KiB
TypeScript

"use client";
import * as React from "react";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Button } from "@/components/ui/button";
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,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import FormDetailTicketing from "@/components/form/ticketing/ticketing-detail-form";
import { useParams } from "next/navigation";
/**
* 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.
*/
type Issue = {
id: string | number;
title?: string;
source?: string;
createdAt?: string;
status?: string;
timeAgo?: string;
};
export default function TicketingTable() {
const params = useParams();
const mediaId = params?.media_id;
const [issues, setIssues] = React.useState<Issue[]>([]);
const [totalElements, setTotalElements] = React.useState<number>(0);
const [totalPages, setTotalPages] = React.useState<number>(1);
const [isSidebarOpen, setIsSidebarOpen] = React.useState<boolean>(true);
const [selectedIssue, setSelectedIssue] = React.useState<Issue | null>(null);
const [search, setSearch] = React.useState<string>("");
const [sortOrder, setSortOrder] = React.useState<"latest" | "oldest">(
"latest"
);
const [page, setPage] = React.useState<number>(1);
const [pageSize, setPageSize] = React.useState<number>(10);
const [selectedTicketId, setSelectedTicketId] = React.useState<string | null>(
null
);
const [selectedMap, setSelectedMap] = React.useState<Record<string, boolean>>(
{}
);
const allSelected = React.useMemo(() => {
if (!issues.length) return false;
return issues.every((i) => selectedMap[String(i.id)]);
}, [issues, selectedMap]);
React.useEffect(() => {
const t = setTimeout(() => {
fetchData();
}, 450);
return () => clearTimeout(t);
}, [search, page, pageSize, sortOrder]);
React.useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
const res = await ticketingPagination(search, pageSize, page - 1, mediaId == 'all' ? "" : mediaId as string);
const data = res?.data?.data;
const content = data?.content || [];
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);
const newMap: Record<string, boolean> = {};
mapped.forEach((m) => {
newMap[String(m.id)] = Boolean(selectedMap[String(m.id)]);
});
setSelectedMap(newMap);
} catch (err) {
console.error("fetchData error", err);
}
}
function toggleSelectAll() {
const next: Record<string, boolean> = {};
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() };
}
const [chatMessages, setChatMessages] = React.useState<
{ id: string; from: "user" | "agent"; text: string; time?: string }[]
>([]);
const [replyText, setReplyText] = React.useState<string>("");
React.useEffect(() => {
if (!selectedIssue) {
setChatMessages([]);
return;
}
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) {
setTimeout(() => {
setSelectedIssue(null);
}, 300);
}
}
function handleTranslate() {
if (!replyText.trim()) return;
setReplyText((t) => t + " (translated)");
}
return (
<div className="w-full min-h-screen bg-gray-50">
{/* Header */}
<div className="px-4 py-4 border-b bg-white">
<div className="max-w-full mx-auto">
<h2 className="text-lg font-semibold">
Semua Ticket: {totalElements}
</h2>
</div>
</div>
<div className="flex h-[calc(100vh-84px)]">
{/* Sidebar */}
<aside
className={cn(
"bg-white border-r transition-all duration-200",
isSidebarOpen ? "w-56" : "w-12"
)}
>
<div className="h-full flex flex-col">
<div className="p-3 flex items-center justify-between border-b">
<div
className={cn(
"text-sm font-medium",
!isSidebarOpen && "hidden"
)}
>
<div
className={cn(
"flex items-center gap-2",
!isSidebarOpen && "justify-center"
)}
>
<span className={cn(!isSidebarOpen && "hidden")}>
All New Issues
</span>
<Badge className={cn(!isSidebarOpen && "hidden")}>
{totalElements}
</Badge>
</div>
</div>
<button
aria-label="toggle sidebar"
className="p-1 rounded-md hover:bg-gray-100"
onClick={() => setIsSidebarOpen((s) => !s)}
title={isSidebarOpen ? "Collapse" : "Open"}
>
{isSidebarOpen ? <ChevronLeft /> : <ChevronRight />}
</button>
</div>
<nav className="flex-1 overflow-auto">
<ul className="text-sm">
<li
className={cn(
"px-3 py-3 border-b flex items-center justify-between",
!isSidebarOpen && "justify-center"
)}
>
<div
className={cn(
"flex items-center gap-2",
!isSidebarOpen && "justify-center"
)}
>
<span className={cn(!isSidebarOpen && "hidden")}>
All New Issues
</span>
<Badge className={cn(!isSidebarOpen && "hidden")}>
{totalElements}
</Badge>
</div>
{/* when collapsed show total */}
{!isSidebarOpen && <Badge>{totalElements}</Badge>}
</li>
{[
"All Issues",
"My Open Issues",
"My Closed Issues",
"Live Chat In Progress",
].map((label) => (
<li key={label} className="px-3 py-4 border-b">
<button className="w-full text-left text-sm">
{label}
</button>
</li>
))}
</ul>
</nav>
</div>
</aside>
{/* Middle: Issue List */}
<section className="flex-shrink-0 w-[420px] bg-white border-r flex flex-col">
{/* top controls: select all, search, sort */}
<div className="p-3 border-b">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={allSelected}
onChange={toggleSelectAll}
className="h-4 w-4"
/>
<div className="flex-1">
<div className="relative">
<Input
placeholder="Cari..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
<Search size={16} />
</div>
</div>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
{sortOrder === "latest" ? "Latest" : "Oldest"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
<DropdownMenuRadioGroup
value={sortOrder}
onValueChange={(value: string) =>
setSortOrder(value as "latest" | "oldest")
}
>
<DropdownMenuRadioItem value="latest">
Latest
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="oldest">
Oldest
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<div className="px-3 py-2 text-xs text-muted-foreground">
Show
</div>
<DropdownMenuRadioGroup
value={String(pageSize)}
onValueChange={(v) => {
setPageSize(Number(v));
setPage(1);
}}
>
<DropdownMenuRadioItem value="10">
10
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
20
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
25
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
{/* list */}
<div className="flex-1 overflow-auto">
<ul>
{issues.length === 0 ? (
<li className="p-6 text-center text-sm text-muted-foreground">
No issues
</li>
) : (
issues.map((it) => {
const key = String(it.id);
const src = sourceToIconLabel(it.source);
return (
<li
key={key}
onClick={() => {
setSelectedIssue(it);
setSelectedTicketId(String(it.id));
}}
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" : ""
)}
>
<div className="flex items-start pt-1">
<input
type="checkbox"
checked={Boolean(selectedMap[key])}
onChange={(e) => {
e.stopPropagation();
toggleSelectOne(it.id);
}}
onClick={(e) => e.stopPropagation()}
className="h-4 w-4 mt-1"
/>
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarFallback>{src.short}</AvatarFallback>
</Avatar>
<div>
<div className="text-sm font-medium">
{(it.title ?? "")
.split(" ")
.slice(0, 25)
.join(" ") +
((it.title ?? "").split(" ").length > 25
? "..."
: "")}
</div>
<div className="text-xs text-muted-foreground flex items-center gap-2">
<span>{src.label}</span>
<span></span>
<span>{it.timeAgo ?? "—"}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className="text-xs">New Issue</Badge>
<div className="text-xs text-muted-foreground">
{it.timeAgo}
</div>
</div>
</div>
</div>
</li>
);
})
)}
</ul>
</div>
{/* pagination simple */}
<div className="p-3 border-t flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{`1-${pageSize} of ${totalElements}`}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
>
Prev
</Button>
<div className="text-sm">
{page} / {totalPages}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
>
Next
</Button>
</div>
</div>
</section>
{/* Right: Chat Panel */}
<main className="flex-1 overflow-y-auto bg-gray-50">
{selectedTicketId ? (
<FormDetailTicketing id={selectedTicketId} />
) : (
<div className="h-full flex items-center justify-center text-gray-400">
Pilih ticket dari sebelah kiri
</div>
)}
</main>
{/* <section className="flex-1 flex flex-col bg-white">
<div className="border-b">
<div className="flex items-center px-4 py-3">
<div className="flex-1">
{selectedIssue ? (
<div className="flex items-center gap-3">
<div className="text-sm font-medium">
{(selectedIssue?.title ?? "")
.split(" ")
.slice(0, 25)
.join(" ") +
((selectedIssue?.title ?? "").split(" ").length > 25
? "..."
: "")}
</div>
<div className="text-xs text-muted-foreground">
• {selectedIssue.source}
</div>
</div>
) : (
<div className="text-sm text-muted-foreground">Chat</div>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-auto p-6">
{!selectedIssue ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<div className="mb-4">
<svg
width="72"
height="72"
viewBox="0 0 24 24"
className="mx-auto opacity-60"
>
<path
fill="currentColor"
d="M12 3C7 3 3 6.6 3 11c0 1.9.8 3.6 2.2 5v3.1L8 17.9c1 .3 2 .5 4 .5 5 0 9-3.6 9-8.1S17 3 12 3z"
/>
</svg>
</div>
<div className="text-sm">
Pilih issue untuk melihat detail
</div>
</div>
</div>
) : (
<div className="space-y-6">
{chatMessages.map((m) => (
<div
key={m.id}
className={cn(
"max-w-[70%] px-4 py-3 rounded-xl relative",
m.from === "agent"
? "ml-auto bg-green-50 text-gray-800"
: "mr-auto bg-blue-50 text-gray-800"
)}
>
<div className="whitespace-pre-wrap leading-relaxed text-sm">
{m.text}
</div>
<div className="mt-2 text-[11px] text-muted-foreground flex items-center justify-end gap-2">
<span>{m.from === "agent" ? "Anda" : "User"}</span>
<span>•</span>
<span>{m.time}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="border-t px-4 py-3">
<div className="max-w-full mx-auto">
<div className="flex flex-col gap-2">
<textarea
placeholder='Enter your reply or type "/" to insert a quick reply'
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
className="w-full border rounded-md p-3 min-h-[64px] resize-none focus:outline-none focus:ring"
/>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleTranslate}
title="Translate"
>
Translate
</Button>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={() => sendReply(true)}
disabled={!replyText.trim()}
>
Kirim & Resolve
</Button>
<Button
size="sm"
onClick={() => sendReply(false)}
disabled={!replyText.trim()}
>
Kirim
</Button>
</div>
</div>
</div>
</div>
</div>
</section> */}
</div>
</div>
);
}
// "use client";
// import * as React from "react";
// import {
// ColumnDef,
// ColumnFiltersState,
// PaginationState,
// SortingState,
// VisibilityState,
// flexRender,
// getCoreRowModel,
// getFilteredRowModel,
// getPaginationRowModel,
// getSortedRowModel,
// useReactTable,
// } from "@tanstack/react-table";
// 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 {
// DropdownMenu,
// 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";
// const TicketingTable = () => {
// const router = useRouter();
// const searchParams = useSearchParams();
// const [dataTable, setDataTable] = React.useState<any[]>([]);
// const [totalData, setTotalData] = React.useState<number>(1);
// const [sorting, setSorting] = React.useState<SortingState>([]);
// const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
// []
// );
// const [search, setSearch] = React.useState("");
// const [columnVisibility, setColumnVisibility] =
// React.useState<VisibilityState>({});
// const [showData, setShowData] = React.useState("10");
// const [rowSelection, setRowSelection] = React.useState({});
// const [pagination, setPagination] = React.useState<PaginationState>({
// 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]);
// React.useEffect(() => {
// fetchData();
// }, [page]);
// async function fetchData() {
// try {
// const res = await ticketingPagination(search, Number(showData), 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;
// });
// console.log("contentData : ", contentData);
// setDataTable(contentData);
// setTotalData(data?.totalElements);
// setTotalPage(data?.totalPages);
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
// return (
// <>
// {" "}
// <div className="flex justify-between py-3">
// {" "}
// <Input
// type="text"
// placeholder="Search"
// onKeyUp={handleKeyUp}
// onKeyDown={handleKeyDown}
// onChange={(e) => setSearch(e.target.value)}
// className="max-w-[300px]"
// />{" "}
// <div className="flex flex-row gap-2">
// {" "}
// <DropdownMenu>
// {" "}
// <DropdownMenuTrigger asChild>
// {" "}
// <Button size="md" variant="outline">
// {" "}
// 1 - {showData} Data{" "}
// </Button>{" "}
// </DropdownMenuTrigger>{" "}
// <DropdownMenuContent className="w-56 text-sm">
// {" "}
// <DropdownMenuRadioGroup
// value={showData}
// onValueChange={setShowData}
// >
// {" "}
// <DropdownMenuRadioItem value="10">
// {" "}
// 1 - 10 Data{" "}
// </DropdownMenuRadioItem>{" "}
// <DropdownMenuRadioItem value="20">
// {" "}
// 1 - 20 Data{" "}
// </DropdownMenuRadioItem>{" "}
// <DropdownMenuRadioItem value="25">
// {" "}
// 1 - 25 Data{" "}
// </DropdownMenuRadioItem>{" "}
// <DropdownMenuRadioItem value="50">
// {" "}
// 1 - 50 Data{" "}
// </DropdownMenuRadioItem>{" "}
// </DropdownMenuRadioGroup>{" "}
// </DropdownMenuContent>{" "}
// </DropdownMenu>{" "}
// </div>{" "}
// </div>{" "}
// <Table className="overflow-hidden">
// {" "}
// <TableHeader>
// {" "}
// {table.getHeaderGroups().map((headerGroup) => (
// <TableRow key={headerGroup.id} className="bg-default-200">
// {" "}
// {headerGroup.headers.map((header) => (
// <TableHead key={header.id}>
// {" "}
// {header.isPlaceholder
// ? null
// : flexRender(
// header.column.columnDef.header,
// header.getContext()
// )}{" "}
// </TableHead>
// ))}{" "}
// </TableRow>
// ))}{" "}
// </TableHeader>{" "}
// <TableBody>
// {" "}
// {table.getRowModel().rows?.length ? (
// table.getRowModel().rows.map((row) => (
// <TableRow
// key={row.id}
// data-state={row.getIsSelected() && "selected"}
// className="h-[75px]"
// >
// {" "}
// {row.getVisibleCells().map((cell) => (
// <TableCell key={cell.id}>
// {" "}
// {flexRender(
// cell.column.columnDef.cell,
// cell.getContext()
// )}{" "}
// </TableCell>
// ))}{" "}
// </TableRow>
// ))
// ) : (
// <TableRow>
// {" "}
// <TableCell colSpan={columns.length} className="h-24 text-center">
// {" "}
// No results.{" "}
// </TableCell>{" "}
// </TableRow>
// )}{" "}
// </TableBody>{" "}
// </Table>{" "}
// <TablePagination
// table={table}
// totalData={totalData}
// totalPage={totalPage}
// />{" "}
// </>
// );
// };
// export default TicketingTable;