From fa7a52a0f303429917fa497727c3c91cbd358bfc Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 29 Apr 2026 02:34:36 +0800 Subject: [PATCH] fix(umkm): dashboard backend - jumlahKategoriTerbanyak, kategoriAktif, growth, trendPersen, mode=week - bump to 0.1.40 Co-Authored-By: Claude Sonnet 4.6 --- .../ekonomi/umkm/dashboard/detailPenjualan.ts | 107 ++++++++++++------ .../_lib/ekonomi/umkm/dashboard/kpi.ts | 86 +++++++------- .../ekonomi/umkm/dashboard/ringSummary.ts | 60 +++++++--- .../_lib/ekonomi/umkm/dashboard/topProduk.ts | 100 ++++++++++++---- 4 files changed, 237 insertions(+), 116 deletions(-) diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/detailPenjualan.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/detailPenjualan.ts index 9abfce97..1a573faa 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/detailPenjualan.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/detailPenjualan.ts @@ -1,49 +1,86 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; +function getWeekRange(offsetWeeks = 0) { + const now = new Date(); + const day = now.getDay(); + const diffToMonday = day === 0 ? -6 : 1 - day; + + const monday = new Date(now); + monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + return { start: monday, end: sunday }; +} + async function umkmDashboardDetailPenjualan(context: Context) { - const periode = (context.query.periode as string) || - `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const mode = context.query.mode as string | undefined; + const isWeek = mode === "week"; + + const periode = + (context.query.periode as string) || + `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; const date = new Date(periode + "-01"); date.setMonth(date.getMonth() - 1); - const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + + const kategoriId = context.query.kategoriId as string | undefined; + const umkmId = context.query.umkmId as string | undefined; + + const whereSkrg = isWeek + ? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null } + : { periode, deletedAt: null }; + + const whereLalu = isWeek + ? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null } + : { periode: periodeLalu, deletedAt: null }; + + // Filter produk berdasarkan kategori dan/atau UMKM + const produkFilter: Record = { deletedAt: null }; + if (kategoriId) produkFilter.kategoriProdukId = kategoriId; + if (umkmId) produkFilter.umkmId = umkmId; try { - // Ambil semua produk yang punya penjualan bulan ini atau bulan lalu const [produkSkrg, produkLalu, allProduks] = await Promise.all([ prisma.penjualanProduk.groupBy({ - by: ['produkId'], - where: { periode, deletedAt: null }, - _sum: { totalNilai: true, jumlah: true } + by: ["produkId"], + where: whereSkrg, + _sum: { totalNilai: true, jumlah: true }, }), prisma.penjualanProduk.groupBy({ - by: ['produkId'], - where: { periode: periodeLalu, deletedAt: null }, - _sum: { totalNilai: true } + by: ["produkId"], + where: whereLalu, + _sum: { totalNilai: true }, }), - // Use PasarDesa prisma.pasarDesa.findMany({ - where: { deletedAt: null }, - select: { id: true, nama: true, stok: true } - }) + where: produkFilter, + select: { id: true, nama: true, stok: true }, + }), ]); - const data = allProduks.map(p => { - const skrgRaw = produkSkrg.find(s => s.produkId === p.id)?._sum || { totalNilai: 0, jumlah: 0 }; - const laluRaw = produkLalu.find(l => l.produkId === p.id)?._sum || { totalNilai: 0 }; - - const skrg = { - totalNilai: (skrgRaw as any).totalNilai || 0, - jumlah: (skrgRaw as any).jumlah || 0 - }; - const lalu = { - totalNilai: (laluRaw as any).totalNilai || 0 - }; - + const data = allProduks.map((p) => { + const skrgRaw = produkSkrg.find((s) => s.produkId === p.id)?._sum || {}; + const laluRaw = produkLalu.find((l) => l.produkId === p.id)?._sum || {}; + + const nilaiSkrg = (skrgRaw as any).totalNilai || 0; + const nilaiLalu = (laluRaw as any).totalNilai || 0; + const jumlah = (skrgRaw as any).jumlah || 0; + let trend = "stable"; - if (skrg.totalNilai > lalu.totalNilai) trend = "up"; - if (skrg.totalNilai < lalu.totalNilai) trend = "down"; + if (nilaiSkrg > nilaiLalu) trend = "up"; + if (nilaiSkrg < nilaiLalu) trend = "down"; + + let trendPersen = 0; + if (nilaiLalu > 0) { + trendPersen = Math.round(((nilaiSkrg - nilaiLalu) / nilaiLalu) * 10000) / 100; + } else if (nilaiSkrg > 0) { + trendPersen = 100; + } let statusStok = "Aman"; if (p.stok < 5) statusStok = "Rendah"; @@ -51,19 +88,17 @@ async function umkmDashboardDetailPenjualan(context: Context) { return { namaProduk: p.nama, - penjualanBulanIni: skrg.totalNilai, - penjualanBulanLalu: lalu.totalNilai, + penjualanBulanIni: nilaiSkrg, + penjualanBulanLalu: nilaiLalu, trend, - volume: skrg.jumlah, + trendPersen, + volume: jumlah, stok: p.stok, - statusStok + statusStok, }; }); - return { - success: true, - data - }; + return { success: true, data }; } catch (e) { console.error("Error di umkmDashboardDetailPenjualan:", e); return { success: false, message: "Gagal mengambil detail penjualan dashboard" }; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts index 31730e50..009eea34 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/kpi.ts @@ -1,44 +1,55 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; +function getWeekRange(offsetWeeks = 0) { + const now = new Date(); + const day = now.getDay(); + const diffToMonday = day === 0 ? -6 : 1 - day; + + const monday = new Date(now); + monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + return { start: monday, end: sunday }; +} + async function umkmDashboardKpi(context: Context) { + const mode = context.query.mode as string | undefined; + const isWeek = mode === "week"; + const periode = (context.query.periode as string) || - `${new Date().getFullYear()}-${String( - new Date().getMonth() + 1 - ).padStart(2, "0")}`; + `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; + + const whereClause = isWeek + ? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null } + : { periode, deletedAt: null }; try { - // KPI utama const [umkmAktif, totalUmkm, omzetBulanan] = await Promise.all([ - prisma.umkm.count({ - where: { isActive: true, deletedAt: null }, - }), - prisma.umkm.count({ - where: { deletedAt: null }, - }), + prisma.umkm.count({ where: { isActive: true, deletedAt: null } }), + prisma.umkm.count({ where: { deletedAt: null } }), prisma.penjualanProduk.aggregate({ - where: { periode, deletedAt: null }, + where: whereClause, _sum: { totalNilai: true }, }), ]); - // ========================= - // 1. Cari kategori dari penjualan - // ========================= + // Cari kategori dengan penjualan terbanyak const salesByCategory = await prisma.penjualanProduk.findMany({ - where: { periode, deletedAt: null }, + where: whereClause, select: { jumlah: true, - produk: { - select: { - kategoriProdukId: true, - }, - }, + produk: { select: { kategoriProdukId: true } }, }, }); let kategoriNama = "-"; + let topCategoryId: string | null = null; if (salesByCategory.length > 0) { const categoryCounts: Record = {}; @@ -46,15 +57,10 @@ async function umkmDashboardKpi(context: Context) { for (const sale of salesByCategory) { const catId = sale.produk.kategoriProdukId; if (!catId) continue; - - categoryCounts[catId] = - (categoryCounts[catId] || 0) + sale.jumlah; + categoryCounts[catId] = (categoryCounts[catId] || 0) + sale.jumlah; } - // cari kategori dengan penjualan tertinggi - let topCategoryId: string | null = null; let maxSales = 0; - for (const [id, count] of Object.entries(categoryCounts)) { if (count > maxSales) { maxSales = count; @@ -67,34 +73,36 @@ async function umkmDashboardKpi(context: Context) { where: { id: topCategoryId }, select: { nama: true }, }); - kategoriNama = kategori?.nama || "-"; } } - // ========================= - // 2. Fallback (kalau tidak ada penjualan) - // ========================= + // Fallback: kategori dari UMKM terbanyak if (kategoriNama === "-") { const kategoriTerbanyakUmkm = await prisma.umkm.groupBy({ by: ["kategoriId"], _count: { _all: true }, - orderBy: { - _count: { kategoriId: "desc" }, - }, + orderBy: { _count: { kategoriId: "desc" } }, take: 1, }); if (kategoriTerbanyakUmkm.length > 0) { + topCategoryId = kategoriTerbanyakUmkm[0].kategoriId; const kategori = await prisma.kategoriProdukUmkm.findUnique({ - where: { id: kategoriTerbanyakUmkm[0].kategoriId }, + where: { id: topCategoryId }, select: { nama: true }, }); - kategoriNama = kategori?.nama || "-"; } } + // Hitung jumlah produk dalam kategori terbanyak + const jumlahKategoriTerbanyak = topCategoryId + ? await prisma.pasarDesa.count({ + where: { kategoriProdukId: topCategoryId, deletedAt: null }, + }) + : 0; + return { success: true, data: { @@ -102,15 +110,13 @@ async function umkmDashboardKpi(context: Context) { totalUmkm, omzetBulanan: omzetBulanan._sum.totalNilai || 0, kategoriTerbanyak: kategoriNama, + jumlahKategoriTerbanyak, }, }; } catch (e) { console.error("Error di umkmDashboardKpi:", e); - return { - success: false, - message: "Gagal mengambil data KPI dashboard", - }; + return { success: false, message: "Gagal mengambil data KPI dashboard" }; } } -export default umkmDashboardKpi; \ No newline at end of file +export default umkmDashboardKpi; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/ringSummary.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/ringSummary.ts index f6fe7425..d6b44a86 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/ringSummary.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/ringSummary.ts @@ -1,35 +1,63 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; +function getWeekRange(offsetWeeks = 0) { + const now = new Date(); + const day = now.getDay(); + const diffToMonday = day === 0 ? -6 : 1 - day; + + const monday = new Date(now); + monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + return { start: monday, end: sunday }; +} + async function umkmDashboardRingSummary(context: Context) { - const periode = (context.query.periode as string) || - `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const mode = context.query.mode as string | undefined; + const isWeek = mode === "week"; + + const periode = + (context.query.periode as string) || + `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; // Hitung periode bulan lalu const date = new Date(periode + "-01"); date.setMonth(date.getMonth() - 1); - const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + + const whereSkrg = isWeek + ? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null } + : { periode, deletedAt: null }; + + const whereLalu = isWeek + ? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null } + : { periode: periodeLalu, deletedAt: null }; try { - const [penjualanSkrg, penjualanLalu, produkAktif, totalTransaksi] = await Promise.all([ + const [penjualanSkrg, penjualanLalu, kategoriAktif, totalTransaksi] = await Promise.all([ prisma.penjualanProduk.aggregate({ - where: { periode, deletedAt: null }, - _sum: { totalNilai: true } + where: whereSkrg, + _sum: { totalNilai: true }, }), prisma.penjualanProduk.aggregate({ - where: { periode: periodeLalu, deletedAt: null }, - _sum: { totalNilai: true } + where: whereLalu, + _sum: { totalNilai: true }, }), - // Count from PasarDesa - prisma.pasarDesa.count({ - where: { isActive: true, deletedAt: null } + // Hitung jumlah kategori aktif + prisma.kategoriProdukUmkm.count({ + where: { isActive: true, deletedAt: null }, }), - prisma.penjualanProduk.count({ where: { periode, deletedAt: null } }) + prisma.penjualanProduk.count({ where: whereSkrg }), ]); const skrg = penjualanSkrg._sum.totalNilai || 0; const lalu = penjualanLalu._sum.totalNilai || 0; - + let persentasePerubahan = 0; if (lalu > 0) { persentasePerubahan = ((skrg - lalu) / lalu) * 100; @@ -42,9 +70,9 @@ async function umkmDashboardRingSummary(context: Context) { data: { totalPenjualan: skrg, persentasePerubahan: Math.round(persentasePerubahan * 100) / 100, - produkAktif, - totalTransaksi - } + kategoriAktif, + totalTransaksi, + }, }; } catch (e) { console.error("Error di umkmDashboardRingSummary:", e); diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/topProduk.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/topProduk.ts index a1398f33..ffc57ad8 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/topProduk.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/topProduk.ts @@ -1,39 +1,91 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; +function getWeekRange(offsetWeeks = 0) { + const now = new Date(); + const day = now.getDay(); + const diffToMonday = day === 0 ? -6 : 1 - day; + + const monday = new Date(now); + monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + return { start: monday, end: sunday }; +} + async function umkmDashboardTopProduk(context: Context) { - const periode = (context.query.periode as string) || - `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const mode = context.query.mode as string | undefined; + const isWeek = mode === "week"; + + const periode = + (context.query.periode as string) || + `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; + + const date = new Date(periode + "-01"); + date.setMonth(date.getMonth() - 1); + const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + + const whereSkrg = isWeek + ? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null } + : { periode, deletedAt: null }; + + const whereLalu = isWeek + ? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null } + : { periode: periodeLalu, deletedAt: null }; try { const topPenjualan = await prisma.penjualanProduk.groupBy({ - by: ['produkId'], - where: { periode, deletedAt: null }, + by: ["produkId"], + where: whereSkrg, _sum: { totalNilai: true, jumlah: true }, - orderBy: { _sum: { totalNilai: 'desc' } }, - take: 3 + orderBy: { _sum: { totalNilai: "desc" } }, + take: 3, }); - const data = await Promise.all(topPenjualan.map(async (item) => { - // Find from PasarDesa now - const produk = await prisma.pasarDesa.findUnique({ - where: { id: item.produkId }, - include: { umkm: true } - }); + // Ambil penjualan periode lalu untuk produk yang sama + const produkIds = topPenjualan.map((p) => p.produkId); + const penjualanLalu = await prisma.penjualanProduk.groupBy({ + by: ["produkId"], + where: { ...whereLalu, produkId: { in: produkIds } }, + _sum: { totalNilai: true }, + }); - return { - namaProduk: produk?.nama || "Unknown", - namaUmkm: produk?.umkm?.nama || "Unknown", - totalPenjualan: item._sum.totalNilai || 0, - jumlahTerjual: item._sum.jumlah || 0, - growth: 0 - }; - })); + const laluMap = new Map( + penjualanLalu.map((p) => [p.produkId, p._sum.totalNilai || 0]) + ); - return { - success: true, - data - }; + const data = await Promise.all( + topPenjualan.map(async (item) => { + const produk = await prisma.pasarDesa.findUnique({ + where: { id: item.produkId }, + include: { umkm: true }, + }); + + const totalSkrg = item._sum.totalNilai || 0; + const totalLalu = laluMap.get(item.produkId) || 0; + + let growth = 0; + if (totalLalu > 0) { + growth = Math.round(((totalSkrg - totalLalu) / totalLalu) * 10000) / 100; + } else if (totalSkrg > 0) { + growth = 100; + } + + return { + namaProduk: produk?.nama || "Unknown", + namaUmkm: produk?.umkm?.nama || "Unknown", + totalPenjualan: totalSkrg, + jumlahTerjual: item._sum.jumlah || 0, + growth, + }; + }) + ); + + return { success: true, data }; } catch (e) { console.error("Error di umkmDashboardTopProduk:", e); return { success: false, message: "Gagal mengambil top produk" };