feat: add export data to excel cta
This commit is contained in:
parent
3992012b16
commit
a5c3737524
|
|
@ -37,6 +37,7 @@
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -6335,6 +6336,15 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
|
@ -6979,6 +6989,19 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
|
@ -7116,6 +7139,15 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -7199,6 +7231,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
|
@ -8653,6 +8697,15 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.33.0",
|
"version": "12.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.33.0.tgz",
|
||||||
|
|
@ -12669,6 +12722,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
|
|
@ -13657,6 +13722,24 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|
@ -13711,6 +13794,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { WordCloud } from "./WordCloud";
|
||||||
import AnalysisClient from "./AnalysisClient";
|
import AnalysisClient from "./AnalysisClient";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import ExportExcel from "./ExportExcel";
|
||||||
|
|
||||||
export default function DashboardClient() {
|
export default function DashboardClient() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -151,7 +152,10 @@ export default function DashboardClient() {
|
||||||
Hasil klasifikasi sentimen ulasan produk laptop
|
Hasil klasifikasi sentimen ulasan produk laptop
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BrandFilter />
|
<div className="flex gap-2 items-center">
|
||||||
|
<BrandFilter />
|
||||||
|
<ExportExcel />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ReviewTable />
|
<ReviewTable />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import { useReviewTable } from "@/src/hooks/useReviewTable";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { downloadAllData } from "@/src/services/report.service";
|
||||||
|
|
||||||
|
export default function ExportExcel() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const selectedBrand = searchParams.get("brand");
|
||||||
|
const { isLoading, data } = useReviewTable(10, selectedBrand);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => downloadAllData(data)}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 border-primary/20 text-primary hover:bg-primary/5"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,288 @@
|
||||||
|
// "use client";
|
||||||
|
|
||||||
|
// import {
|
||||||
|
// Table,
|
||||||
|
// TableBody,
|
||||||
|
// TableCell,
|
||||||
|
// TableHead,
|
||||||
|
// TableHeader,
|
||||||
|
// TableRow,
|
||||||
|
// } from "../../components/ui/table";
|
||||||
|
// import { Badge } from "../../components/ui/badge";
|
||||||
|
// import { Inbox, Loader2 } from "lucide-react";
|
||||||
|
// import getSentimentBadge from "./SentimentBadge";
|
||||||
|
// import { useReviewTable } from "@/src/hooks/useReviewTable";
|
||||||
|
// import {
|
||||||
|
// Pagination,
|
||||||
|
// PaginationContent,
|
||||||
|
// PaginationEllipsis,
|
||||||
|
// PaginationItem,
|
||||||
|
// PaginationLink,
|
||||||
|
// PaginationNext,
|
||||||
|
// PaginationPrevious,
|
||||||
|
// } from "../ui/pagination";
|
||||||
|
// import { useSearchParams } from "next/navigation";
|
||||||
|
// import { getVisiblePages } from "@/src/utils/datas";
|
||||||
|
// import { exportToExcel } from "@/src/services/report.service";
|
||||||
|
|
||||||
|
// export function ReviewTable() {
|
||||||
|
// const searchParams = useSearchParams();
|
||||||
|
// const selectedBrand = searchParams.get("brand");
|
||||||
|
// const { currentData, isLoading, pagination } = useReviewTable(
|
||||||
|
// 10,
|
||||||
|
// selectedBrand,
|
||||||
|
// );
|
||||||
|
// const { currentPage, totalPages, nextPage, prevPage, goToPage } = pagination;
|
||||||
|
// const visiblePage = getVisiblePages({ totalPages, currentPage });
|
||||||
|
|
||||||
|
// if (isLoading) {
|
||||||
|
// return (
|
||||||
|
// <div className="flex h-75 w-full flex-col items-center justify-center gap-2 rounded-xl border bg-card text-muted-foreground">
|
||||||
|
// <Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
// <p className="text-sm">Memuat data ulasan...</p>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="rounded-xl border bg-card">
|
||||||
|
// <Table>
|
||||||
|
// <TableHeader>
|
||||||
|
// <TableRow className="hover:bg-transparent">
|
||||||
|
// <TableHead className="w-62.5">Produk</TableHead>
|
||||||
|
// <TableHead className="w-auto min-w-75">
|
||||||
|
// Ulasan & Kata Kunci
|
||||||
|
// </TableHead>
|
||||||
|
// <TableHead className="w-30 whitespace-nowrap">Tanggal</TableHead>
|
||||||
|
// <TableHead className="w-30">Sentimen</TableHead>
|
||||||
|
// <TableHead className="w-50">Confidence Score</TableHead>
|
||||||
|
// </TableRow>
|
||||||
|
// </TableHeader>
|
||||||
|
// <TableBody>
|
||||||
|
// {currentData.length === 0 ? (
|
||||||
|
// <TableRow>
|
||||||
|
// <TableCell colSpan={5} className="h-75 text-center">
|
||||||
|
// <div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||||
|
// <div className="rounded-full bg-muted">
|
||||||
|
// <Inbox className="h-8 w-8" />
|
||||||
|
// </div>
|
||||||
|
// <p className="text-lg font-medium text-foreground">
|
||||||
|
// Belum ada data
|
||||||
|
// </p>
|
||||||
|
// <p className="text-sm">
|
||||||
|
// Belum ada ulasan yang dianalisis oleh sistem.
|
||||||
|
// </p>
|
||||||
|
// </div>
|
||||||
|
// </TableCell>
|
||||||
|
// </TableRow>
|
||||||
|
// ) : (
|
||||||
|
// currentData.map((review, index) => (
|
||||||
|
// <TableRow
|
||||||
|
// key={review.id || index}
|
||||||
|
// className="group animate-in fade-in transition-colors hover:bg-muted/40"
|
||||||
|
// style={{
|
||||||
|
// animationDelay: `${index * 50}ms`,
|
||||||
|
// animationFillMode: "backwards",
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <TableCell className="align-top">
|
||||||
|
// <div className="flex flex-col gap-1.5">
|
||||||
|
// <div className="flex items-center gap-2">
|
||||||
|
// <span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20">
|
||||||
|
// {/* Tambahkan .name di sini */}
|
||||||
|
// {review.product?.brand?.name || "Generic"}
|
||||||
|
// </span>
|
||||||
|
// </div>
|
||||||
|
// <span className="text-sm font-medium leading-tight text-foreground line-clamp-2">
|
||||||
|
// {review.product?.name || "Unknown Product"}
|
||||||
|
// </span>
|
||||||
|
// </div>
|
||||||
|
// </TableCell>
|
||||||
|
|
||||||
|
// <TableCell className="align-top">
|
||||||
|
// <div className="flex flex-col gap-3">
|
||||||
|
// <p className="text-sm leading-relaxed text-muted-foreground line-clamp-3 group-hover:text-foreground transition-colors">
|
||||||
|
// {review.content}
|
||||||
|
// </p>
|
||||||
|
|
||||||
|
// {review.keywords && review.keywords.length > 0 && (
|
||||||
|
// <div className="flex flex-wrap gap-1.5">
|
||||||
|
// {review.keywords.slice(0, 5).map((k, i) => (
|
||||||
|
// <Badge
|
||||||
|
// key={i}
|
||||||
|
// variant="secondary"
|
||||||
|
// className="h-5 px-1.5 text-[10px] font-normal text-muted-foreground border-border bg-muted group-hover:bg-background transition-all"
|
||||||
|
// >
|
||||||
|
// {k}
|
||||||
|
// </Badge>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// </TableCell>
|
||||||
|
|
||||||
|
// <TableCell className="align-top whitespace-nowrap">
|
||||||
|
// <span className="text-xs text-muted-foreground font-medium">
|
||||||
|
// {review.createdAt
|
||||||
|
// ? new Date(review.createdAt).toLocaleDateString("id-ID", {
|
||||||
|
// day: "numeric",
|
||||||
|
// month: "short",
|
||||||
|
// year: "numeric",
|
||||||
|
// })
|
||||||
|
// : "-"}
|
||||||
|
// </span>
|
||||||
|
// </TableCell>
|
||||||
|
|
||||||
|
// <TableCell className="align-top">
|
||||||
|
// {getSentimentBadge(review.sentiment ?? null)}
|
||||||
|
// </TableCell>
|
||||||
|
|
||||||
|
// <TableCell className="align-top">
|
||||||
|
// <span className="font-mono text-sm font-semibold text-foreground">
|
||||||
|
// {review.confidenceScore
|
||||||
|
// ? `${(review.confidenceScore * 100).toFixed(1)}%`
|
||||||
|
// : "-"}
|
||||||
|
// </span>
|
||||||
|
// </TableCell>
|
||||||
|
// {/* <TableCell className="align-top ">
|
||||||
|
// <DropdownMenu>
|
||||||
|
// <DropdownMenuTrigger asChild className="cursor-pointer">
|
||||||
|
// <EllipsisVertical className="w-4 h-4" />
|
||||||
|
// </DropdownMenuTrigger>
|
||||||
|
// <DropdownMenuContent
|
||||||
|
// align="center"
|
||||||
|
// className={`w-max bg-card border-border shadow-md `}
|
||||||
|
// >
|
||||||
|
// <DropdownMenuItem className="cursor-pointer gap-2 focus:bg-sentiment-neutral-light focus:text-sentiment-neutral transition-colors hover:text-primary">
|
||||||
|
// <Pencil />
|
||||||
|
// <span>Edit</span>
|
||||||
|
// </DropdownMenuItem>
|
||||||
|
// <DropdownMenuItem className="flex cursor-pointer gap-2 text-destructive focus:bg-red-500 focus:text-white transition-colors">
|
||||||
|
// <Trash />
|
||||||
|
// <span>Delete</span>
|
||||||
|
// </DropdownMenuItem>
|
||||||
|
// </DropdownMenuContent>
|
||||||
|
// </DropdownMenu>
|
||||||
|
// </TableCell> */}
|
||||||
|
// </TableRow>
|
||||||
|
// ))
|
||||||
|
// )}
|
||||||
|
// </TableBody>
|
||||||
|
// </Table>
|
||||||
|
|
||||||
|
// {/* {totalPages > 1 && (
|
||||||
|
// <div className="border-t bg-muted/20 px-6 py-4">
|
||||||
|
// <Pagination className="justify-center sm:justify-end">
|
||||||
|
// <PaginationContent>
|
||||||
|
// <PaginationItem>
|
||||||
|
// <PaginationPrevious
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// prevPage();
|
||||||
|
// }}
|
||||||
|
// className={
|
||||||
|
// currentPage === 1
|
||||||
|
// ? "pointer-events-none opacity-50"
|
||||||
|
// : "cursor-pointer"
|
||||||
|
// }
|
||||||
|
// />
|
||||||
|
// </PaginationItem>
|
||||||
|
|
||||||
|
// {[...Array(totalPages)].map((_, i) => (
|
||||||
|
// <PaginationItem key={i + 1}>
|
||||||
|
// <PaginationLink
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// goToPage(i + 1);
|
||||||
|
// }}
|
||||||
|
// isActive={currentPage === i + 1}
|
||||||
|
// >
|
||||||
|
// {i + 1}
|
||||||
|
// </PaginationLink>
|
||||||
|
// </PaginationItem>
|
||||||
|
// ))}
|
||||||
|
|
||||||
|
// <PaginationItem>
|
||||||
|
// <PaginationNext
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// nextPage();
|
||||||
|
// }}
|
||||||
|
// className={
|
||||||
|
// currentPage === totalPages
|
||||||
|
// ? "pointer-events-none opacity-50"
|
||||||
|
// : "cursor-pointer"
|
||||||
|
// }
|
||||||
|
// />
|
||||||
|
// </PaginationItem>
|
||||||
|
// </PaginationContent>
|
||||||
|
// </Pagination>
|
||||||
|
// </div>
|
||||||
|
// )} */}
|
||||||
|
|
||||||
|
// {totalPages > 1 && (
|
||||||
|
// <div className="border-t bg-muted/20 px-6 py-4">
|
||||||
|
// <Pagination className="justify-center sm:justify-end">
|
||||||
|
// <PaginationContent>
|
||||||
|
// <PaginationItem>
|
||||||
|
// <PaginationPrevious
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// prevPage();
|
||||||
|
// }}
|
||||||
|
// className={
|
||||||
|
// currentPage === 1
|
||||||
|
// ? "pointer-events-none opacity-50"
|
||||||
|
// : "cursor-pointer hover:bg-[#F8FBFF] hover:text-primary"
|
||||||
|
// }
|
||||||
|
// />
|
||||||
|
// </PaginationItem>
|
||||||
|
|
||||||
|
// {visiblePage.map((page, index) => (
|
||||||
|
// <PaginationItem key={index}>
|
||||||
|
// {page === "..." ? (
|
||||||
|
// <PaginationEllipsis className="hover:cursor-not-allowed" />
|
||||||
|
// ) : (
|
||||||
|
// <PaginationLink
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// goToPage(page as number);
|
||||||
|
// }}
|
||||||
|
// isActive={currentPage === page}
|
||||||
|
// >
|
||||||
|
// {page}
|
||||||
|
// </PaginationLink>
|
||||||
|
// )}
|
||||||
|
// </PaginationItem>
|
||||||
|
// ))}
|
||||||
|
|
||||||
|
// <PaginationItem>
|
||||||
|
// <PaginationNext
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// nextPage();
|
||||||
|
// }}
|
||||||
|
// className={
|
||||||
|
// currentPage === totalPages
|
||||||
|
// ? "pointer-events-none opacity-50"
|
||||||
|
// : "cursor-pointer hover:bg-primary hover:text-card"
|
||||||
|
// }
|
||||||
|
// />
|
||||||
|
// </PaginationItem>
|
||||||
|
// </PaginationContent>
|
||||||
|
// </Pagination>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -31,7 +316,7 @@ export function ReviewTable() {
|
||||||
10,
|
10,
|
||||||
selectedBrand,
|
selectedBrand,
|
||||||
);
|
);
|
||||||
const { currentPage, totalPages, nextPage, prevPage, goToPage } = pagination;
|
const { currentPage, totalPages } = pagination;
|
||||||
const visiblePage = getVisiblePages({ totalPages, currentPage });
|
const visiblePage = getVisiblePages({ totalPages, currentPage });
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
@ -44,240 +329,173 @@ export function ReviewTable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border bg-card">
|
<div className="space-y-4">
|
||||||
<Table>
|
<div className="rounded-xl border bg-card">
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableHeader>
|
||||||
<TableHead className="w-62.5">Produk</TableHead>
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="w-auto min-w-75">
|
<TableHead className="w-62.5">Produk</TableHead>
|
||||||
Ulasan & Kata Kunci
|
<TableHead className="w-auto min-w-75 text-center">
|
||||||
</TableHead>
|
Ulasan & Kata Kunci
|
||||||
<TableHead className="w-30 whitespace-nowrap">Tanggal</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-30">Sentimen</TableHead>
|
<TableHead className="w-30 whitespace-nowrap">Tanggal</TableHead>
|
||||||
<TableHead className="w-50">Confidence Score</TableHead>
|
<TableHead className="w-30 text-center">Sentimen</TableHead>
|
||||||
</TableRow>
|
<TableHead className="w-50 text-center">
|
||||||
</TableHeader>
|
Confidence Score
|
||||||
<TableBody>
|
</TableHead>
|
||||||
{currentData.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="h-75 text-center">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
|
||||||
<div className="rounded-full bg-muted">
|
|
||||||
<Inbox className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-medium text-foreground">
|
|
||||||
Belum ada data
|
|
||||||
</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
Belum ada ulasan yang dianalisis oleh sistem.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
currentData.map((review, index) => (
|
<TableBody>
|
||||||
<TableRow
|
{currentData.length === 0 ? (
|
||||||
key={review.id || index}
|
<TableRow>
|
||||||
className="group animate-in fade-in transition-colors hover:bg-muted/40"
|
<TableCell colSpan={5} className="h-75 text-center">
|
||||||
style={{
|
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||||
animationDelay: `${index * 50}ms`,
|
<div className="rounded-full bg-muted">
|
||||||
animationFillMode: "backwards",
|
<Inbox className="h-8 w-8" />
|
||||||
}}
|
</div>
|
||||||
>
|
<p className="text-lg font-medium text-foreground">
|
||||||
<TableCell className="align-top">
|
Belum ada data
|
||||||
<div className="flex flex-col gap-1.5">
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<p className="text-sm">
|
||||||
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20">
|
Belum ada ulasan yang dianalisis oleh sistem.
|
||||||
{/* Tambahkan .name di sini */}
|
</p>
|
||||||
{review.product?.brand?.name || "Generic"}
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
currentData.map((review, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={review.id || index}
|
||||||
|
className="group animate-in fade-in transition-colors hover:bg-muted/40"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${index * 50}ms`,
|
||||||
|
animationFillMode: "backwards",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell className="align-top">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20">
|
||||||
|
{review.product?.brand?.name || "Generic"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium leading-tight text-foreground line-clamp-2">
|
||||||
|
{review.product?.name || "Unknown Product"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium leading-tight text-foreground line-clamp-2">
|
</TableCell>
|
||||||
{review.product?.name || "Unknown Product"}
|
|
||||||
|
<TableCell className="align-top">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground line-clamp-3 group-hover:text-foreground transition-colors">
|
||||||
|
{review.content}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{review.keywords && review.keywords.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{review.keywords.slice(0, 5).map((k, i) => (
|
||||||
|
<Badge
|
||||||
|
key={i}
|
||||||
|
variant="secondary"
|
||||||
|
className="h-5 px-1.5 text-[10px] font-normal text-muted-foreground border-border bg-muted group-hover:bg-background transition-all"
|
||||||
|
>
|
||||||
|
{k}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="align-top whitespace-nowrap">
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">
|
||||||
|
{review.createdAt
|
||||||
|
? new Date(review.createdAt).toLocaleDateString(
|
||||||
|
"id-ID",
|
||||||
|
{
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top text-center">
|
||||||
<div className="flex flex-col gap-3">
|
{getSentimentBadge(review.sentiment ?? null)}
|
||||||
<p className="text-sm leading-relaxed text-muted-foreground line-clamp-3 group-hover:text-foreground transition-colors">
|
</TableCell>
|
||||||
{review.content}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{review.keywords && review.keywords.length > 0 && (
|
<TableCell className="align-top text-center">
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<span className="font-mono text-sm font-semibold text-foreground">
|
||||||
{review.keywords.slice(0, 5).map((k, i) => (
|
{review.confidenceScore
|
||||||
<Badge
|
? `${(review.confidenceScore * 100).toFixed(1)}%`
|
||||||
key={i}
|
: "-"}
|
||||||
variant="secondary"
|
</span>
|
||||||
className="h-5 px-1.5 text-[10px] font-normal text-muted-foreground border-border bg-muted group-hover:bg-background transition-all"
|
</TableCell>
|
||||||
>
|
</TableRow>
|
||||||
{k}
|
))
|
||||||
</Badge>
|
)}
|
||||||
))}
|
</TableBody>
|
||||||
</div>
|
</Table>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="align-top whitespace-nowrap">
|
{totalPages > 1 && (
|
||||||
<span className="text-xs text-muted-foreground font-medium">
|
<div className="border-t bg-muted/20 px-6 py-4">
|
||||||
{review.createdAt
|
<Pagination className="justify-center sm:justify-end">
|
||||||
? new Date(review.createdAt).toLocaleDateString("id-ID", {
|
<PaginationContent>
|
||||||
day: "numeric",
|
<PaginationItem>
|
||||||
month: "short",
|
<PaginationPrevious
|
||||||
year: "numeric",
|
|
||||||
})
|
|
||||||
: "-"}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="align-top">
|
|
||||||
{getSentimentBadge(review.sentiment ?? null)}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="align-top">
|
|
||||||
<span className="font-mono text-sm font-semibold text-foreground">
|
|
||||||
{review.confidenceScore
|
|
||||||
? `${(review.confidenceScore * 100).toFixed(1)}%`
|
|
||||||
: "-"}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
{/* <TableCell className="align-top ">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild className="cursor-pointer">
|
|
||||||
<EllipsisVertical className="w-4 h-4" />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="center"
|
|
||||||
className={`w-max bg-card border-border shadow-md `}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem className="cursor-pointer gap-2 focus:bg-sentiment-neutral-light focus:text-sentiment-neutral transition-colors hover:text-primary">
|
|
||||||
<Pencil />
|
|
||||||
<span>Edit</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="flex cursor-pointer gap-2 text-destructive focus:bg-red-500 focus:text-white transition-colors">
|
|
||||||
<Trash />
|
|
||||||
<span>Delete</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell> */}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{/* {totalPages > 1 && (
|
|
||||||
<div className="border-t bg-muted/20 px-6 py-4">
|
|
||||||
<Pagination className="justify-center sm:justify-end">
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
prevPage();
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
currentPage === 1
|
|
||||||
? "pointer-events-none opacity-50"
|
|
||||||
: "cursor-pointer"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{[...Array(totalPages)].map((_, i) => (
|
|
||||||
<PaginationItem key={i + 1}>
|
|
||||||
<PaginationLink
|
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
goToPage(i + 1);
|
pagination.prevPage();
|
||||||
}}
|
}}
|
||||||
isActive={currentPage === i + 1}
|
className={
|
||||||
>
|
currentPage === 1
|
||||||
{i + 1}
|
? "pointer-events-none opacity-50"
|
||||||
</PaginationLink>
|
: "cursor-pointer hover:bg-[#F8FBFF] hover:text-primary"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
|
||||||
|
|
||||||
<PaginationItem>
|
{visiblePage.map((page, index) => (
|
||||||
<PaginationNext
|
<PaginationItem key={index}>
|
||||||
href="#"
|
{page === "..." ? (
|
||||||
onClick={(e) => {
|
<PaginationEllipsis className="hover:cursor-not-allowed" />
|
||||||
e.preventDefault();
|
) : (
|
||||||
nextPage();
|
<PaginationLink
|
||||||
}}
|
href="#"
|
||||||
className={
|
onClick={(e) => {
|
||||||
currentPage === totalPages
|
e.preventDefault();
|
||||||
? "pointer-events-none opacity-50"
|
pagination.goToPage(page as number);
|
||||||
: "cursor-pointer"
|
}}
|
||||||
}
|
isActive={currentPage === page}
|
||||||
/>
|
>
|
||||||
</PaginationItem>
|
{page}
|
||||||
</PaginationContent>
|
</PaginationLink>
|
||||||
</Pagination>
|
)}
|
||||||
</div>
|
</PaginationItem>
|
||||||
)} */}
|
))}
|
||||||
|
|
||||||
{totalPages > 1 && (
|
<PaginationItem>
|
||||||
<div className="border-t bg-muted/20 px-6 py-4">
|
<PaginationNext
|
||||||
<Pagination className="justify-center sm:justify-end">
|
href="#"
|
||||||
<PaginationContent>
|
onClick={(e) => {
|
||||||
<PaginationItem>
|
e.preventDefault();
|
||||||
<PaginationPrevious
|
pagination.nextPage();
|
||||||
href="#"
|
}}
|
||||||
onClick={(e) => {
|
className={
|
||||||
e.preventDefault();
|
currentPage === totalPages
|
||||||
prevPage();
|
? "pointer-events-none opacity-50"
|
||||||
}}
|
: "cursor-pointer hover:bg-primary hover:text-card"
|
||||||
className={
|
}
|
||||||
currentPage === 1
|
/>
|
||||||
? "pointer-events-none opacity-50"
|
|
||||||
: "cursor-pointer hover:bg-[#F8FBFF] hover:text-primary"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{visiblePage.map((page, index) => (
|
|
||||||
<PaginationItem key={index}>
|
|
||||||
{page === "..." ? (
|
|
||||||
<PaginationEllipsis className="hover:cursor-not-allowed" />
|
|
||||||
) : (
|
|
||||||
<PaginationLink
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
goToPage(page as number);
|
|
||||||
}}
|
|
||||||
isActive={currentPage === page}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
)}
|
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
<PaginationItem>
|
</div>
|
||||||
<PaginationNext
|
)}
|
||||||
href="#"
|
</div>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
nextPage();
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
currentPage === totalPages
|
|
||||||
? "pointer-events-none opacity-50"
|
|
||||||
: "cursor-pointer hover:bg-primary hover:text-card"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { ApiResponse, ReviewItem } from "../types";
|
import { ApiResponse, ReviewItem } from "../types";
|
||||||
|
import { PaginationService } from "../services/review.service";
|
||||||
|
|
||||||
export const useReviewTable = (
|
export const useReviewTable = (
|
||||||
itemsPerPage: number = 10,
|
itemsPerPage: number = 10,
|
||||||
|
|
@ -57,16 +58,19 @@ export const useReviewTable = (
|
||||||
}, [data, currentPage, itemsPerPage, selectedBrand]);
|
}, [data, currentPage, itemsPerPage, selectedBrand]);
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (currentPage < totalPages) setCurrentPage((prev) => prev + 1);
|
setCurrentPage((prev) => PaginationService.getNextPage(prev, totalPages));
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevPage = () => {
|
const prevPage = () => {
|
||||||
if (currentPage > 1) setCurrentPage((prev) => prev - 1);
|
setCurrentPage((prev) => PaginationService.getPrevPage(prev));
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPage = (pageNumber: number) => {
|
const goToPage = (pageNumber: number) => {
|
||||||
if (pageNumber >= 1 && pageNumber <= totalPages) setCurrentPage(pageNumber);
|
setCurrentPage(PaginationService.getValidPage(pageNumber, totalPages));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
data,
|
||||||
currentData,
|
currentData,
|
||||||
isLoading,
|
isLoading,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import { ReviewItem } from "../types";
|
||||||
|
|
||||||
export const reportService = async () => {
|
export const reportService = async () => {
|
||||||
const response = await prisma.model.findMany({
|
const response = await prisma.model.findMany({
|
||||||
|
|
@ -20,6 +22,54 @@ export const reportService = async () => {
|
||||||
if (!response || response.length === 0) {
|
if (!response || response.length === 0) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportToExcel = (data: ReviewItem[], fileName: string) => {
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Sentiment Analysis");
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, `${fileName}.xlsx`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadAllData = (data: ReviewItem[]) => {
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
"ID",
|
||||||
|
"Product Name",
|
||||||
|
"Brand",
|
||||||
|
"Review Text",
|
||||||
|
"Sentiment",
|
||||||
|
"Rating",
|
||||||
|
"Date",
|
||||||
|
];
|
||||||
|
|
||||||
|
const csvRows = data.map((item) => [
|
||||||
|
item.id,
|
||||||
|
`"${item.product?.name || ""}"`,
|
||||||
|
item.product?.brand?.name || "",
|
||||||
|
`"${item.content?.replace(/"/g, '""') || ""}"`,
|
||||||
|
item.sentiment || "",
|
||||||
|
item.confidenceScore || 0,
|
||||||
|
item.createdAt ? new Date(item.createdAt).toLocaleDateString() : "",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(","),
|
||||||
|
...csvRows.map((row) => row.join(",")),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", `all_reviews_${new Date().getTime()}.csv`);
|
||||||
|
link.style.visibility = "hidden";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,22 @@ export const getReviewService = async (email: string) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return review;
|
return review;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PaginationService = {
|
||||||
|
getNextPage: (currentPage: number, totalPages: number): number => {
|
||||||
|
return currentPage < totalPages ? currentPage + 1 : currentPage;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPrevPage: (currentPage: number): number => {
|
||||||
|
return currentPage > 1 ? currentPage - 1 : currentPage;
|
||||||
|
},
|
||||||
|
|
||||||
|
getValidPage: (pageNumber: number, totalPages: number): number => {
|
||||||
|
if (pageNumber < 1) return 1;
|
||||||
|
if (pageNumber > totalPages) return totalPages;
|
||||||
|
return pageNumber;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue