fix(umkm): dashboard backend - jumlahKategoriTerbanyak, kategoriAktif, growth, trendPersen, mode=week - bump to 0.1.40

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 02:34:36 +08:00
parent ef237aea2f
commit fa7a52a0f3
4 changed files with 237 additions and 116 deletions

View File

@@ -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<string, unknown> = { 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" };

View File

@@ -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<string, number> = {};
@@ -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;
export default umkmDashboardKpi;

View File

@@ -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);

View File

@@ -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" };