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:
@@ -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" };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" };
|
||||
|
||||
Reference in New Issue
Block a user