refactor: optimize business logic into service layer.

This commit is contained in:
Mahen 2026-02-22 07:36:17 +07:00
parent cac55f1805
commit c42e719381
12 changed files with 388 additions and 321 deletions

View File

@ -1,5 +1,5 @@
import { authOptions } from "@/src/app/api/auth/[...nextauth]/route"; import { authOptions } from "@/src/app/api/auth/[...nextauth]/route";
import { ApiHandler } from "@/src/types"; import { ApiHandler, ServerActionHandler } from "@/src/types";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
@ -17,3 +17,18 @@ export function withAuth(handler: ApiHandler) {
return handler(req, context, session); return handler(req, context, session);
}; };
} }
export function withActionAuth<T, Args extends any[] = any[]>(
handler: ServerActionHandler<T, Args>,
) {
return async (...args: Args): Promise<T | null> => {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
console.log("Unauthorized: User belum login");
return null;
}
return handler(session, ...args);
};
}

14
lib/withBody.ts Normal file
View File

@ -0,0 +1,14 @@
import { BodyData } from "@/src/types";
import { NextResponse } from "next/server";
export function withBody(handler: BodyData) {
return async (req: Request) => {
const body = await req.json();
const { url } = body;
if (!url || !url.includes("tokopedia.com")) {
return NextResponse.json({ error: "URL tidak valid" }, { status: 400 });
}
return handler(req, body);
};
}

View File

@ -1,16 +1,12 @@
import { withBody } from "@/lib/withBody";
import { scrapeTokopediaProduct } from "@/src/services/scrape.service"; import { scrapeTokopediaProduct } from "@/src/services/scrape.service";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function POST(request: Request) { export const POST = withBody(async (_req, body) => {
try { try {
const body = await request.json(); const result = await scrapeTokopediaProduct(body.url);
const { url } = body;
if (!url || !url.includes("tokopedia.com")) { console.log(result);
return NextResponse.json({ error: "URL tidak valid" }, { status: 400 });
}
const result = await scrapeTokopediaProduct(url);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@ -19,4 +15,4 @@ export async function POST(request: Request) {
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }
} });

View File

@ -1,29 +1,13 @@
"use server"; "use server";
import prisma from "@/lib/prisma";
import { getServerSession } from "next-auth"; import { withActionAuth } from "@/lib/withAuth";
import { notFound } from "next/navigation"; import { getAnalysisData } from "@/src/services/analyze.service";
import { authOptions } from "../../api/auth/[...nextauth]/route"; import { formatBrandStats } from "@/src/services/brand.service";
import { reportService } from "@/src/services/report.service";
export const getClassificationReport = async () => { export const getClassificationReport = async () => {
try { try {
const response = await prisma.model.findMany({ const response = await reportService();
select: {
modelName: true,
description: true,
accuracy: true,
macroF1: true,
f1Negative: true,
f1Neutral: true,
isActive: true,
},
orderBy: {
createdAt: "desc",
},
});
if (!response || response.length === 0) {
return notFound();
}
return response; return response;
} catch (error) { } catch (error) {
@ -32,85 +16,17 @@ export const getClassificationReport = async () => {
} }
}; };
export const getTotalBrandAnalysis = async () => { export const getTotalBrandAnalysis = withActionAuth(async (session) => {
try { try {
const session = await getServerSession(authOptions); const email = session.user?.email as string;
if (!session?.user?.email) { const userAnalysis = await getAnalysisData(email);
console.log("User belum login");
return null;
}
const userAnalyses = await prisma.analysis.findMany({ const formattedBrands = formatBrandStats(userAnalysis);
where: {
user: {
email: session.user.email,
},
},
include: {
product: {
select: {
id: true,
brand: true,
_count: {
select: {
reviews: {
where: {
user: {
email: session.user.email,
},
},
},
},
},
},
},
},
});
const countedProductIds = new Set<number>(); return { formattedBrands, userAnalysis };
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) { } catch (error) {
console.error("Gagal mengambil data review:", error); console.error("Gagal mengambil data review:", error);
return []; return [];
} }
}; });

View File

@ -1,193 +1,193 @@
// Sample data for the sentiment analysis dashboard // // Sample data for the sentiment analysis dashboard
// Based on Tokopedia laptop reviews analysis // // Based on Tokopedia laptop reviews analysis
import { CheckCircle2, Cpu, Target, Zap } from "lucide-react"; // import { CheckCircle2, Cpu, Target, Zap } from "lucide-react";
export const sentimentDistribution = [ // export const sentimentDistribution = [
{ name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" }, // { name: "Positif", value: 2456, color: "hsl(158, 64%, 42%)" },
{ name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" }, // { name: "Negatif", value: 607, color: "hsl(0, 72%, 51%)" },
{ name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" }, // { name: "Netral", value: 382, color: "hsl(43, 74%, 49%)" },
]; // ];
export const trendData = [ // export const trendData = [
{ date: "Jan", positif: 580, negatif: 220, netral: 150 }, // { date: "Jan", positif: 580, negatif: 220, netral: 150 },
{ date: "Feb", positif: 620, negatif: 245, netral: 175 }, // { date: "Feb", positif: 620, negatif: 245, netral: 175 },
{ date: "Mar", positif: 750, negatif: 280, netral: 190 }, // { date: "Mar", positif: 750, negatif: 280, netral: 190 },
{ date: "Apr", positif: 690, negatif: 310, netral: 165 }, // { date: "Apr", positif: 690, negatif: 310, netral: 165 },
{ date: "Mei", positif: 820, negatif: 265, netral: 210 }, // { date: "Mei", positif: 820, negatif: 265, netral: 210 },
{ date: "Jun", positif: 780, negatif: 240, netral: 195 }, // { date: "Jun", positif: 780, negatif: 240, netral: 195 },
{ date: "Jul", positif: 850, negatif: 290, netral: 220 }, // { date: "Jul", positif: 850, negatif: 290, netral: 220 },
{ date: "Agu", positif: 720, negatif: 255, netral: 180 }, // { date: "Agu", positif: 720, negatif: 255, netral: 180 },
{ date: "Sep", positif: 680, negatif: 230, netral: 165 }, // { date: "Sep", positif: 680, negatif: 230, netral: 165 },
{ date: "Okt", positif: 540, negatif: 195, netral: 145 }, // { date: "Okt", positif: 540, negatif: 195, netral: 145 },
{ date: "Nov", positif: 520, negatif: 175, netral: 150 }, // { date: "Nov", positif: 520, negatif: 175, netral: 150 },
{ date: "Des", positif: 474, negatif: 140, netral: 136 }, // { date: "Des", positif: 474, negatif: 140, netral: 136 },
]; // ];
export const wordCloudData = [ // export const wordCloudData = [
{ text: "barang", value: 1086, sentiment: "positive" as const }, // { text: "barang", value: 1086, sentiment: "positive" as const },
{ text: "sesuai", value: 570, sentiment: "positive" as const }, // { text: "sesuai", value: 570, sentiment: "positive" as const },
{ text: "bagus", value: 523, sentiment: "positive" as const }, // { text: "bagus", value: 523, sentiment: "positive" as const },
{ text: "seller", value: 478, sentiment: "positive" as const }, // { text: "seller", value: 478, sentiment: "positive" as const },
{ text: "cepat", value: 471, sentiment: "positive" as const }, // { text: "cepat", value: 471, sentiment: "positive" as const },
{ text: "aman", value: 453, sentiment: "positive" as const }, // { text: "aman", value: 453, sentiment: "positive" as const },
{ text: "laptop", value: 451, sentiment: "positive" as const }, // { text: "laptop", value: 451, sentiment: "positive" as const },
{ text: "kirim", value: 449, sentiment: "positive" as const }, // { text: "kirim", value: 449, sentiment: "positive" as const },
{ text: "baik", value: 396, sentiment: "positive" as const }, // { text: "baik", value: 396, sentiment: "positive" as const },
{ text: "mulus", value: 364, sentiment: "positive" as const }, // { text: "mulus", value: 364, sentiment: "positive" as const },
{ text: "barang", value: 181, sentiment: "negative" as const }, // { text: "barang", value: 181, sentiment: "negative" as const },
{ text: "kirim", value: 177, sentiment: "negative" as const }, // { text: "kirim", value: 177, sentiment: "negative" as const },
{ text: "laptop", value: 129, sentiment: "negative" as const }, // { text: "laptop", value: 129, sentiment: "negative" as const },
{ text: "beli", value: 124, sentiment: "negative" as const }, // { text: "beli", value: 124, sentiment: "negative" as const },
{ text: "lebih", value: 72, sentiment: "negative" as const }, // { text: "lebih", value: 72, sentiment: "negative" as const },
{ text: "jual", value: 62, sentiment: "negative" as const }, // { text: "jual", value: 62, sentiment: "negative" as const },
{ text: "baru", value: 60, sentiment: "negative" as const }, // { text: "baru", value: 60, sentiment: "negative" as const },
{ text: "lalu", value: 59, sentiment: "negative" as const }, // { text: "lalu", value: 59, sentiment: "negative" as const },
{ text: "sesuai", value: 56, sentiment: "negative" as const }, // { text: "sesuai", value: 56, sentiment: "negative" as const },
{ text: "tahun", value: 56, sentiment: "negative" as const }, // { text: "tahun", value: 56, sentiment: "negative" as const },
{ text: "kirim", value: 106, sentiment: "neutral" as const }, // { text: "kirim", value: 106, sentiment: "neutral" as const },
{ text: "barang", value: 96, sentiment: "neutral" as const }, // { text: "barang", value: 96, sentiment: "neutral" as const },
{ text: "laptop", value: 78, sentiment: "neutral" as const }, // { text: "laptop", value: 78, sentiment: "neutral" as const },
{ text: "sesuai", value: 48, sentiment: "neutral" as const }, // { text: "sesuai", value: 48, sentiment: "neutral" as const },
{ text: "bagus", value: 47, sentiment: "neutral" as const }, // { text: "bagus", value: 47, sentiment: "neutral" as const },
{ text: "lebih", value: 45, sentiment: "neutral" as const }, // { text: "lebih", value: 45, sentiment: "neutral" as const },
{ text: "kurang", value: 44, sentiment: "neutral" as const }, // { text: "kurang", value: 44, sentiment: "neutral" as const },
{ text: "beli", value: 44, sentiment: "neutral" as const }, // { text: "beli", value: 44, sentiment: "neutral" as const },
{ text: "baik", value: 33, sentiment: "neutral" as const }, // { text: "baik", value: 33, sentiment: "neutral" as const },
{ text: "baru", value: 32, sentiment: "neutral" as const }, // { text: "baru", value: 32, sentiment: "neutral" as const },
]; // ];
export const brandData = [ // export const brandData = [
{ name: "ASUS", count: 3245 }, // { name: "ASUS", count: 3245 },
{ name: "Lenovo", count: 2890 }, // { name: "Lenovo", count: 2890 },
{ name: "HP", count: 2456 }, // { name: "HP", count: 2456 },
{ name: "Acer", count: 2134 }, // { name: "Acer", count: 2134 },
{ name: "Dell", count: 1725 }, // { name: "Dell", count: 1725 },
]; // ];
export const reviewData = [ // export const reviewData = [
{ // {
id: "1", // id: "1",
product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7", // product: "ASUS VivoBook 15 X515EA Intel Core i5-1135G7",
brand: "ASUS", // brand: "ASUS",
review: // review:
"Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.", // "Laptop sangat bagus, performa cepat untuk kerja kantoran. Layar jernih dan keyboard nyaman dipakai mengetik seharian. Pengiriman juga cepat dan aman.",
rating: 5, // rating: 5,
sentiment: "positif" as const, // sentiment: "positif" as const,
date: "2 hari yang lalu", // date: "2 hari yang lalu",
confidence: 0.945, // confidence: 0.945,
}, // },
{ // {
id: "2", // id: "2",
product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U", // product: "Lenovo IdeaPad Slim 3 AMD Ryzen 5 5500U",
brand: "Lenovo", // brand: "Lenovo",
review: // review:
"Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.", // "Produk sesuai deskripsi. Build quality oke, performa lancar untuk multitasking ringan. Baterai awet bisa 6-7 jam pemakaian normal.",
rating: 4, // rating: 4,
sentiment: "positif" as const, // sentiment: "positif" as const,
date: "3 hari yang lalu", // date: "3 hari yang lalu",
confidence: 0.887, // confidence: 0.887,
}, // },
{ // {
id: "3", // id: "3",
product: "HP 14s-dq5001TU Intel Core i5-1235U", // product: "HP 14s-dq5001TU Intel Core i5-1235U",
brand: "HP", // brand: "HP",
review: // review:
"Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.", // "Kecewa dengan produk ini. Baru dipakai 2 minggu sudah sering hang dan restart sendiri. Kipas juga berisik sekali padahal hanya buka browser.",
rating: 2, // rating: 2,
sentiment: "negatif" as const, // sentiment: "negatif" as const,
date: "4 hari yang lalu", // date: "4 hari yang lalu",
confidence: 0.923, // confidence: 0.923,
}, // },
{ // {
id: "4", // id: "4",
product: "Acer Aspire 5 A515-57 Intel Core i5-1235U", // product: "Acer Aspire 5 A515-57 Intel Core i5-1235U",
brand: "Acer", // brand: "Acer",
review: // review:
"Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.", // "Laptop oke lah untuk harga segini. Tidak terlalu cepat tapi juga tidak lemot. Cocok untuk mahasiswa dengan budget terbatas.",
rating: 3, // rating: 3,
sentiment: "netral" as const, // sentiment: "netral" as const,
date: "5 hari yang lalu", // date: "5 hari yang lalu",
confidence: 0.812, // confidence: 0.812,
}, // },
{ // {
id: "5", // id: "5",
product: "Dell Inspiron 15 3520 Intel Core i3-1215U", // product: "Dell Inspiron 15 3520 Intel Core i3-1215U",
brand: "Dell", // brand: "Dell",
review: // review:
"Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.", // "Sangat puas dengan pembelian ini! Laptop premium dengan harga terjangkau. Build quality solid, keyboard backlit, dan layar anti-glare sangat membantu.",
rating: 5, // rating: 5,
sentiment: "positif" as const, // sentiment: "positif" as const,
date: "1 minggu yang lalu", // date: "1 minggu yang lalu",
confidence: 0.956, // confidence: 0.956,
}, // },
{ // {
id: "6", // id: "6",
product: "ASUS TUF Gaming F15 FX506HF RTX 2050", // product: "ASUS TUF Gaming F15 FX506HF RTX 2050",
brand: "ASUS", // brand: "ASUS",
review: // review:
"Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.", // "Gaming laptop yang worth it! Main game AAA lancar di medium-high setting. Thermal management bagus, tidak terlalu panas saat gaming marathon.",
rating: 5, // rating: 5,
sentiment: "positif" as const, // sentiment: "positif" as const,
date: "1 minggu yang lalu", // date: "1 minggu yang lalu",
confidence: 0.934, // confidence: 0.934,
}, // },
{ // {
id: "7", // id: "7",
product: "Lenovo V14 G3 AMD Ryzen 3 5300U", // product: "Lenovo V14 G3 AMD Ryzen 3 5300U",
brand: "Lenovo", // brand: "Lenovo",
review: // review:
"Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.", // "Laptop datang dalam kondisi rusak, layar ada garis horizontal. Sudah komplain ke seller tapi respon lambat. Sangat mengecewakan.",
rating: 1, // rating: 1,
sentiment: "negatif" as const, // sentiment: "negatif" as const,
date: "1 minggu yang lalu", // date: "1 minggu yang lalu",
confidence: 0.967, // confidence: 0.967,
}, // },
{ // {
id: "8", // id: "8",
product: "HP Pavilion 14-dv2045TX Intel Core i5", // product: "HP Pavilion 14-dv2045TX Intel Core i5",
brand: "HP", // brand: "HP",
review: // review:
"Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.", // "Desain elegan dan performa mumpuni. Cocok untuk pekerja mobile yang butuh laptop stylish. Speaker B&O juga keren suaranya.",
rating: 4, // rating: 4,
sentiment: "positif" as const, // sentiment: "positif" as const,
date: "2 minggu yang lalu", // date: "2 minggu yang lalu",
confidence: 0.891, // confidence: 0.891,
}, // },
]; // ];
export const modelData = { // export const modelData = {
baseline: { // baseline: {
name: "Model XGBoost (Baseline)", // name: "Model XGBoost (Baseline)",
metrics: [ // metrics: [
{ label: "Accuracy", value: "80.0%", icon: Target }, // { label: "Accuracy", value: "80.0%", icon: Target },
{ label: "Macro F1-Score", value: "56.0%", icon: Cpu }, // { label: "Macro F1-Score", value: "56.0%", icon: Cpu },
{ label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 }, // { label: "F1-Negatif", value: "61.0%", icon: CheckCircle2 },
{ label: "F1-Netral", value: "16.0%", icon: Zap }, // { label: "F1-Netral", value: "16.0%", icon: Zap },
], // ],
description: // description:
"Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.", // "Model awal menggunakan parameter default XGBoost (learning_rate=0.3, max_depth=6) pada dataset yang tidak seimbang.",
}, // },
tuned: { // tuned: {
name: "Model XGBoost (Tuned)", // name: "Model XGBoost (Tuned)",
metrics: [ // metrics: [
{ label: "Accuracy", value: "81.0%", icon: Target }, // { label: "Accuracy", value: "81.0%", icon: Target },
{ label: "Macro F1-Score", value: "58.0%", icon: Cpu }, // { label: "Macro F1-Score", value: "58.0%", icon: Cpu },
{ label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, // { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
{ label: "F1-Netral", value: "17.0%", icon: Zap }, // { label: "F1-Netral", value: "17.0%", icon: Zap },
], // ],
description: // description:
"Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.", // "Model dengan optimasi Hyperparameter menggunakan Grid Search untuk mencari kombinasi learning_rate dan max_depth terbaik.",
}, // },
optimized: { // optimized: {
name: "Model XGBoost (Optimized)", // name: "Model XGBoost (Optimized)",
metrics: [ // metrics: [
{ label: "Accuracy", value: "82.0%", icon: Target }, // { label: "Accuracy", value: "82.0%", icon: Target },
{ label: "Macro F1-Score", value: "61.0%", icon: Cpu }, // { label: "Macro F1-Score", value: "61.0%", icon: Cpu },
{ label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 }, // { label: "F1-Negatif", value: "65.0%", icon: CheckCircle2 },
{ label: "F1-Netral", value: "27.0%", icon: Zap }, // { label: "F1-Netral", value: "27.0%", icon: Zap },
], // ],
description: // description:
"Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.", // "Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.",
}, // },
}; // };

View File

@ -1,37 +1,17 @@
"use server"; "use server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]/route";
import prisma from "@/lib/prisma";
export const getAnotherUserData = async () => { import { withActionAuth } from "@/lib/withAuth";
import { getAnotherUserDataService } from "@/src/services/users.service";
export const getAnotherUserData = withActionAuth(async (session) => {
try { try {
const session = await getServerSession(authOptions); const email = (await session.user?.email) as string;
if (!session?.user?.email) return null; const userData = await getAnotherUserDataService(email);
const userData = await prisma.user.findUnique({
where: {
email: session.user.email,
},
select: {
name: true,
bio: true,
preference: {
select: {
id: true,
profession: true,
preferredBrand: true,
preferredOS: true,
budgetMin: true,
budgetMax: true,
},
},
},
});
return userData; return userData;
} catch (error) { } catch (error) {
console.error("Error fetching user data:", error); console.error("Error fetching user data:", error);
return null; return null;
} }
}; });

View File

@ -11,8 +11,10 @@ export const useBrandFilter = () => {
const fetchBrands = async () => { const fetchBrands = async () => {
try { try {
const data = await getTotalBrandAnalysis(); const data = await getTotalBrandAnalysis();
if (data) { if (data && "formattedBrands" in data) {
setBrands(data); setBrands(data.formattedBrands);
} else {
setBrands([]);
} }
} catch (error) { } catch (error) {
console.error("Gagal memuat filter brand", error); console.error("Gagal memuat filter brand", error);

View File

@ -1,3 +1,5 @@
import prisma from "@/lib/prisma";
export const scrapeProduct = async (url: string) => { export const scrapeProduct = async (url: string) => {
const res = await fetch("/api/scrape", { const res = await fetch("/api/scrape", {
method: "POST", method: "POST",
@ -31,3 +33,34 @@ export const getAIRecommendation = async (payload: {
return await aiRes.json(); return await aiRes.json();
}; };
export const getAnalysisData = async (email: string) => {
const userAnalyses = await prisma.analysis.findMany({
where: {
user: {
email: email,
},
},
include: {
product: {
select: {
id: true,
brand: true,
_count: {
select: {
reviews: {
where: {
user: {
email: email,
},
},
},
},
},
},
},
},
});
return userAnalyses;
};

View File

@ -0,0 +1,44 @@
import { AnalysisData } from "../types";
export const formatBrandStats = (userAnalysis: AnalysisData[]) => {
const countedProductIds = new Set<number>();
const brandCounts = userAnalysis.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;
};

View File

@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
export const reportService = async () => {
const response = await prisma.model.findMany({
select: {
modelName: true,
description: true,
accuracy: true,
macroF1: true,
f1Negative: true,
f1Neutral: true,
isActive: true,
},
orderBy: {
createdAt: "desc",
},
});
if (!response || response.length === 0) {
return notFound();
}
return response;
};

View File

@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
export const getAnotherUserDataService = async (email: string) => {
const userData = await prisma.user.findUnique({
where: {
email: email,
},
select: {
name: true,
bio: true,
preference: {
select: {
id: true,
profession: true,
preferredBrand: true,
preferredOS: true,
budgetMin: true,
budgetMax: true,
},
},
},
});
return userData;
};

View File

@ -268,3 +268,20 @@ export type WordCloudReview = {
keywords: string[]; keywords: string[];
sentiment: Sentiment; sentiment: Sentiment;
}; };
export type ServerActionHandler<T, Args extends any[] = any[]> = (
session: Session,
...args: Args
) => Promise<T>;
export type AnalysisData = {
product?: {
id: number;
brand: string | null;
_count?: {
reviews: number;
};
};
};
export type BodyData = (req: Request, body: any) => Promise<NextResponse>;