feat: add get brand review total count
This commit is contained in:
parent
5c4749e853
commit
cba88c53e1
|
|
@ -1,41 +1,40 @@
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(_request: Request) {
|
// export async function POST(_request: Request) {
|
||||||
try {
|
// try {
|
||||||
const products = [
|
// const products = [
|
||||||
{ name: "ZenBook 14", brand: "ASUS" },
|
// { name: "ZenBook 14", brand: "ASUS" },
|
||||||
{ name: "Swift 3", brand: "Acer" },
|
// { name: "Swift 3", brand: "Acer" },
|
||||||
{ name: "Surface Laptop 5", brand: "Microsoft" },
|
// { name: "Surface Laptop 5", brand: "Microsoft" },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const result = await prisma.product.createMany({
|
// const result = await prisma.product.createMany({
|
||||||
data: products,
|
// data: products,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return NextResponse.json(
|
// return NextResponse.json(
|
||||||
{
|
// {
|
||||||
message: "Booking successful",
|
// message: "Booking successful",
|
||||||
data: result,
|
// data: result,
|
||||||
},
|
// },
|
||||||
{ status: 201 },
|
// { status: 201 },
|
||||||
);
|
// );
|
||||||
} catch (error: unknown) {
|
// } catch (error: unknown) {
|
||||||
console.error("Create product error:", error);
|
// console.error("Create product error:", error);
|
||||||
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
// if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
// return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return NextResponse.json(
|
// return NextResponse.json(
|
||||||
{ error: "Internal Server Error" },
|
// { error: "Internal Server Error" },
|
||||||
{ status: 500 },
|
// { status: 500 },
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export async function POST(_request: Request) {
|
||||||
content:
|
content:
|
||||||
"Laptop ini sangat ringan dan performanya cepat untuk kerja harian.",
|
"Laptop ini sangat ringan dan performanya cepat untuk kerja harian.",
|
||||||
keywords: ["ringan", "cepat", "kerja"],
|
keywords: ["ringan", "cepat", "kerja"],
|
||||||
sentiment: Sentiment.positive,
|
sentiment: Sentiment.POSITIVE,
|
||||||
confidenceScore: 0.92,
|
confidenceScore: 0.92,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -21,7 +21,7 @@ export async function POST(_request: Request) {
|
||||||
modelId: 1,
|
modelId: 1,
|
||||||
content: "Baterainya awet, tapi harganya cukup mahal.",
|
content: "Baterainya awet, tapi harganya cukup mahal.",
|
||||||
keywords: ["baterai", "awet", "mahal"],
|
keywords: ["baterai", "awet", "mahal"],
|
||||||
sentiment: Sentiment.neutral,
|
sentiment: Sentiment.NEUTRAL,
|
||||||
confidenceScore: 0.75,
|
confidenceScore: 0.75,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -29,7 +29,7 @@ export async function POST(_request: Request) {
|
||||||
modelId: 1,
|
modelId: 1,
|
||||||
content: "Performa kurang stabil dan sering panas.",
|
content: "Performa kurang stabil dan sering panas.",
|
||||||
keywords: ["performa", "panas", "stabil"],
|
keywords: ["performa", "panas", "stabil"],
|
||||||
sentiment: Sentiment.negative,
|
sentiment: Sentiment.NEGATIVE,
|
||||||
confidenceScore: 0.88,
|
confidenceScore: 0.88,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"use server";
|
"use server";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { authOptions } from "../../api/auth/[...nextauth]/route";
|
||||||
|
|
||||||
export const getClassificationReport = async () => {
|
export const getClassificationReport = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -28,3 +30,80 @@ export const getClassificationReport = async () => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTotalBrandAnalysis = async () => {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
console.log("User belum login");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAnalyses = await prisma.analysis.findMany({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
email: session.user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
product: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
brand: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
reviews: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const countedProductIds = new Set<number>();
|
||||||
|
|
||||||
|
const brandCounts = userAnalyses.reduce(
|
||||||
|
(acc: Record<string, number>, analysis) => {
|
||||||
|
const productId = analysis.product?.id;
|
||||||
|
const rawBrand = analysis.product?.brand || "Unknown";
|
||||||
|
const reviewCount = analysis.product?._count?.reviews || 0;
|
||||||
|
|
||||||
|
if (productId && countedProductIds.has(productId)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (productId) {
|
||||||
|
countedProductIds.add(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedBrand = rawBrand
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||||
|
|
||||||
|
if (!acc[formattedBrand]) {
|
||||||
|
acc[formattedBrand] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[formattedBrand] += reviewCount;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedBrands = Object.entries(brandCounts).map(
|
||||||
|
([name, count]) => ({
|
||||||
|
name,
|
||||||
|
count,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
formattedBrands.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
return formattedBrands;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal mengambil data review:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BrandFilterProps } from "@/src/types";
|
import { BrandFilterProps } from "@/src/types";
|
||||||
|
import { useBrandFilter } from "@/src/hooks/useBrandFilter";
|
||||||
|
|
||||||
export function BrandFilter({
|
export function BrandFilter({
|
||||||
brands,
|
|
||||||
selectedBrand,
|
selectedBrand,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: BrandFilterProps) {
|
}: Omit<BrandFilterProps, "brands">) {
|
||||||
|
const { brands, isLoading, totalCount } = useBrandFilter();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-muted-foreground animate-pulse">
|
||||||
|
Memuat brand...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -17,8 +29,9 @@ export function BrandFilter({
|
||||||
: "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground",
|
: "border-border bg-card text-muted-foreground hover:border-primary/50 hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Semua ({brands.reduce((sum, b) => sum + b.count, 0).toLocaleString()})
|
Semua ({totalCount.toLocaleString()})
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{brands.map((brand) => (
|
{brands.map((brand) => (
|
||||||
<button
|
<button
|
||||||
key={brand.name}
|
key={brand.name}
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ export default function DashboardClient() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BrandFilter
|
<BrandFilter
|
||||||
brands={brandData}
|
// brands={brandData}
|
||||||
selectedBrand={selectedBrand}
|
selectedBrand={selectedBrand}
|
||||||
onSelect={setSelectedBrand}
|
onSelect={setSelectedBrand}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const getSentimentBadge = (sentiment: Review["sentiment"]) => {
|
const getSentimentBadge = (sentiment: Review["sentiment"]) => {
|
||||||
const styles: Record<Review["sentiment"], string> = {
|
const styles: Record<Review["sentiment"], string> = {
|
||||||
positive: "sentiment-positive",
|
POSITIVE: "sentiment-positive",
|
||||||
negative: "sentiment-negative",
|
NEGATIVE: "sentiment-negative",
|
||||||
neutral: "sentiment-neutral",
|
NEUTRAL: "sentiment-neutral",
|
||||||
};
|
};
|
||||||
|
|
||||||
const labels: Record<Review["sentiment"], string> = {
|
const labels: Record<Review["sentiment"], string> = {
|
||||||
positive: "Positif",
|
POSITIVE: "Positif",
|
||||||
negative: "Negatif",
|
NEGATIVE: "Negatif",
|
||||||
neutral: "Netral",
|
NEUTRAL: "Netral",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { AnalysisResults } from "../types";
|
import { AnalysisResults } from "../types";
|
||||||
|
|
||||||
export const useAnalyseText = () => {
|
export const useAnalyseText = () => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [url1, setUrl1] = useState("");
|
const [url1, setUrl1] = useState("");
|
||||||
const [url2, setUrl2] = useState("");
|
const [url2, setUrl2] = useState("");
|
||||||
const [url3, setUrl3] = useState("");
|
const [url3, setUrl3] = useState("");
|
||||||
|
|
@ -12,6 +15,13 @@ export const useAnalyseText = () => {
|
||||||
const [showField, setShowField] = useState(false);
|
const [showField, setShowField] = useState(false);
|
||||||
|
|
||||||
const handleAnalyze = async () => {
|
const handleAnalyze = async () => {
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
alert(
|
||||||
|
"Anda harus login terlebih dahulu untuk menyimpan riwayat analisis.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
|
|
||||||
|
|
@ -53,6 +63,7 @@ export const useAnalyseText = () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
user_email: session.user.email,
|
||||||
profession: profession,
|
profession: profession,
|
||||||
candidates: candidates,
|
candidates: candidates,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getTotalBrandAnalysis } from "../app/dashboard/lib/actions";
|
||||||
|
|
||||||
|
export const useBrandFilter = () => {
|
||||||
|
const [brands, setBrands] = useState<{ name: string; count: number }[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBrands = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getTotalBrandAnalysis();
|
||||||
|
if (data) {
|
||||||
|
setBrands(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal memuat filter brand", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBrands();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const totalCount = brands.reduce((sum, b) => sum + (b?.count || 0), 0);
|
||||||
|
|
||||||
|
return { brands, isLoading, totalCount };
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { LucideIcon } from "lucide-react";
|
import { LucideIcon } from "lucide-react";
|
||||||
import Brand from "@prisma/client";
|
import Brand, { Sentiment } from "@prisma/client";
|
||||||
|
|
||||||
export interface ModelDB {
|
export interface ModelDB {
|
||||||
modelName: string;
|
modelName: string;
|
||||||
|
|
@ -27,7 +27,7 @@ interface Brand {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrandFilterProps {
|
export interface BrandFilterProps {
|
||||||
brands: Brand[];
|
// brands: Brand[];
|
||||||
selectedBrand: string | null;
|
selectedBrand: string | null;
|
||||||
onSelect: (brand: string | null) => void;
|
onSelect: (brand: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ export interface ReviewTableProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnalysisResult {
|
export interface AnalysisResult {
|
||||||
sentiment: "positif" | "negatif" | "netral";
|
sentiment: Sentiment;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -142,8 +142,6 @@ export interface ApiResponse {
|
||||||
data: ReviewItem[];
|
data: ReviewItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Sentiment = "positive" | "negative" | "neutral";
|
|
||||||
|
|
||||||
export type WordItem = {
|
export type WordItem = {
|
||||||
text: string;
|
text: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue