Compare commits
8 Commits
faf78064c7
...
f31ab0eda5
| Author | SHA1 | Date | |
|---|---|---|---|
| f31ab0eda5 | |||
| 0517d50c8e | |||
| fa7a52a0f3 | |||
| ef237aea2f | |||
| f6107e971d | |||
| 550961d524 | |||
| 34d49fa073 | |||
| 1631e273a4 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.35",
|
"version": "0.1.42",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Migrate PasarDesa.kategoriProdukId FK from KategoriProduk to KategoriProdukUmkm
|
||||||
|
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriProdukId_fkey";
|
||||||
|
|
||||||
|
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProdukUmkm"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Seed KategoriProdukUmkm with any KategoriProduk entries referenced by PasarDesa
|
||||||
|
-- that are not yet in KategoriProdukUmkm
|
||||||
|
INSERT INTO "KategoriProdukUmkm" ("id", "nama", "createdAt", "updatedAt", "deletedAt", "isActive")
|
||||||
|
SELECT DISTINCT kp.id, kp.nama, kp."createdAt", kp."updatedAt", kp."deletedAt", kp."isActive"
|
||||||
|
FROM "KategoriProduk" kp
|
||||||
|
WHERE kp.id IN (
|
||||||
|
SELECT DISTINCT "kategoriProdukId" FROM "PasarDesa"
|
||||||
|
WHERE "kategoriProdukId" IS NOT NULL
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "KategoriProdukUmkm" WHERE id = kp.id
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Step 1: Seed KategoriProdukUmkm from KategoriProduk for any PasarDesa-referenced entries not yet present
|
||||||
|
INSERT INTO "KategoriProdukUmkm" ("id", "nama", "createdAt", "updatedAt", "deletedAt", "isActive")
|
||||||
|
SELECT DISTINCT kp.id, kp.nama, kp."createdAt", kp."updatedAt", kp."deletedAt", kp."isActive"
|
||||||
|
FROM "KategoriProduk" kp
|
||||||
|
WHERE kp.id IN (
|
||||||
|
SELECT DISTINCT "kategoriProdukId" FROM "PasarDesa"
|
||||||
|
WHERE "kategoriProdukId" IS NOT NULL
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "KategoriProdukUmkm" WHERE id = kp.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Step 2: Make kategoriProdukId nullable to handle orphaned/legacy data
|
||||||
|
ALTER TABLE "PasarDesa" ALTER COLUMN "kategoriProdukId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- Step 3: Null out any remaining orphaned references (not in KategoriProdukUmkm)
|
||||||
|
UPDATE "PasarDesa"
|
||||||
|
SET "kategoriProdukId" = NULL
|
||||||
|
WHERE "kategoriProdukId" IS NOT NULL
|
||||||
|
AND "kategoriProdukId" NOT IN (SELECT id FROM "KategoriProdukUmkm");
|
||||||
@@ -1445,8 +1445,8 @@ model PasarDesa {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
|
kategoriProduk KategoriProdukUmkm? @relation(fields: [kategoriProdukId], references: [id])
|
||||||
kategoriProdukId String
|
kategoriProdukId String?
|
||||||
KategoriToPasar KategoriToPasar[]
|
KategoriToPasar KategoriToPasar[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1458,7 +1458,6 @@ model KategoriProduk {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
KategoriToPasar KategoriToPasar[]
|
KategoriToPasar KategoriToPasar[]
|
||||||
PasarDesa PasarDesa[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriToPasar {
|
model KategoriToPasar {
|
||||||
@@ -2448,6 +2447,7 @@ model KategoriProdukUmkm {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Umkm Umkm[]
|
Umkm Umkm[]
|
||||||
|
PasarDesa PasarDesa[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -493,23 +493,31 @@ export const umkmState = proxy({
|
|||||||
summary: { data: null as any, loading: false },
|
summary: { data: null as any, loading: false },
|
||||||
topProduk: { data: [] as any[], loading: false },
|
topProduk: { data: [] as any[], loading: false },
|
||||||
detail: { data: [] as any[], loading: false },
|
detail: { data: [] as any[], loading: false },
|
||||||
async loadAll(periode = "") {
|
mode: "month" as "week" | "month",
|
||||||
|
async loadAll(periode = "", mode?: "week" | "month", kategoriId = "", umkmId = "") {
|
||||||
const p = periode || `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
const p = periode || `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
const m = mode ?? this.mode;
|
||||||
|
const modeParam = m === "week" ? "&mode=week" : "";
|
||||||
|
const detailFilter = [
|
||||||
|
kategoriId ? `&kategoriId=${kategoriId}` : "",
|
||||||
|
umkmId ? `&umkmId=${umkmId}` : "",
|
||||||
|
].join("");
|
||||||
this.kpi.loading = true;
|
this.kpi.loading = true;
|
||||||
this.summary.loading = true;
|
this.summary.loading = true;
|
||||||
this.topProduk.loading = true;
|
this.topProduk.loading = true;
|
||||||
this.detail.loading = true;
|
this.detail.loading = true;
|
||||||
try {
|
try {
|
||||||
const [kpi, sum, top, det] = await Promise.all([
|
const [kpi, sum, top, det] = await Promise.all([
|
||||||
fetch(`/api/ekonomi/umkm/dashboard/kpi?periode=${p}`).then(r => r.json()),
|
fetch(`/api/ekonomi/umkm/dashboard/kpi?periode=${p}${modeParam}`).then(r => r.json()),
|
||||||
fetch(`/api/ekonomi/umkm/dashboard/ringkasan-penjualan?periode=${p}`).then(r => r.json()),
|
fetch(`/api/ekonomi/umkm/dashboard/ringkasan-penjualan?periode=${p}${modeParam}`).then(r => r.json()),
|
||||||
fetch(`/api/ekonomi/umkm/dashboard/top-produk?periode=${p}`).then(r => r.json()),
|
fetch(`/api/ekonomi/umkm/dashboard/top-produk?periode=${p}${modeParam}`).then(r => r.json()),
|
||||||
fetch(`/api/ekonomi/umkm/dashboard/detail-penjualan?periode=${p}`).then(r => r.json())
|
fetch(`/api/ekonomi/umkm/dashboard/detail-penjualan?periode=${p}${modeParam}${detailFilter}`).then(r => r.json()),
|
||||||
]);
|
]);
|
||||||
if (kpi.success) this.kpi.data = kpi.data;
|
if (kpi.success) this.kpi.data = kpi.data;
|
||||||
if (sum.success) this.summary.data = sum.data;
|
if (sum.success) this.summary.data = sum.data;
|
||||||
if (top.success) this.topProduk.data = top.data;
|
if (top.success) this.topProduk.data = top.data;
|
||||||
if (det.success) this.detail.data = det.data;
|
if (det.success) this.detail.data = det.data;
|
||||||
|
this.mode = m;
|
||||||
} catch (e) { console.error(e); } finally {
|
} catch (e) { console.error(e); } finally {
|
||||||
this.kpi.loading = false;
|
this.kpi.loading = false;
|
||||||
this.summary.loading = false;
|
this.summary.loading = false;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -16,10 +18,11 @@ import {
|
|||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Badge
|
SegmentedControl,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowUpRight, IconArrowDownRight, IconMinus } from '@tabler/icons-react';
|
import { IconArrowUpRight, IconArrowDownRight, IconMinus } from '@tabler/icons-react';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
@@ -27,10 +30,25 @@ import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
|||||||
function UmkmDashboard() {
|
function UmkmDashboard() {
|
||||||
const state = useProxy(umkmState.dashboard);
|
const state = useProxy(umkmState.dashboard);
|
||||||
|
|
||||||
|
const [kategoriId, setKategoriId] = useState("");
|
||||||
|
const [umkmId, setUmkmId] = useState("");
|
||||||
|
const [kategoriList, setKategoriList] = useState<{ value: string; label: string }[]>([]);
|
||||||
|
const [umkmList, setUmkmList] = useState<{ value: string; label: string }[]>([]);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.loadAll();
|
state.loadAll();
|
||||||
|
fetch('/api/ekonomi/kategoriproduk/find-many-all').then(r => r.json()).then(res => {
|
||||||
|
if (res.success) setKategoriList(res.data.map((k: any) => ({ value: k.id, label: k.nama })));
|
||||||
|
});
|
||||||
|
fetch('/api/ekonomi/umkm/find-many-all').then(r => r.json()).then(res => {
|
||||||
|
if (res.success) setUmkmList(res.data.map((u: any) => ({ value: u.id, label: u.nama })));
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.kpi.data) state.loadAll("", state.mode, kategoriId, umkmId);
|
||||||
|
}, [kategoriId, umkmId]);
|
||||||
|
|
||||||
if (state.kpi.loading || !state.kpi.data) {
|
if (state.kpi.loading || !state.kpi.data) {
|
||||||
return <Skeleton height={400} radius="md" />;
|
return <Skeleton height={400} radius="md" />;
|
||||||
}
|
}
|
||||||
@@ -42,6 +60,18 @@ function UmkmDashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={4}>Update Penjualan Produk</Title>
|
||||||
|
<SegmentedControl
|
||||||
|
value={state.mode}
|
||||||
|
onChange={(v) => state.loadAll("", v as "week" | "month", kategoriId, umkmId)}
|
||||||
|
data={[
|
||||||
|
{ label: 'Minggu ini', value: 'week' },
|
||||||
|
{ label: 'Bulan ini', value: 'month' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
|
||||||
<KpiCard title="UMKM Aktif" value={kpi.umkmAktif} subValue={`Total: ${kpi.totalUmkm}`} />
|
<KpiCard title="UMKM Aktif" value={kpi.umkmAktif} subValue={`Total: ${kpi.totalUmkm}`} />
|
||||||
<KpiCard
|
<KpiCard
|
||||||
@@ -49,8 +79,12 @@ function UmkmDashboard() {
|
|||||||
value={`Rp ${kpi.omzetBulanan.toLocaleString()}`}
|
value={`Rp ${kpi.omzetBulanan.toLocaleString()}`}
|
||||||
trend={summary?.persentasePerubahan}
|
trend={summary?.persentasePerubahan}
|
||||||
/>
|
/>
|
||||||
<KpiCard title="Produk Aktif" value={summary?.produkAktif || 0} />
|
<KpiCard title="Kategori Aktif" value={summary?.kategoriAktif || 0} subValue="kategori" />
|
||||||
<KpiCard title="Kategori Populer" value={kpi.kategoriTerbanyak} />
|
<KpiCard
|
||||||
|
title="UMKM Terbanyak"
|
||||||
|
value={kpi.jumlahKategoriTerbanyak || 0}
|
||||||
|
subValue={kpi.kategoriTerbanyak}
|
||||||
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -59,7 +93,7 @@ function UmkmDashboard() {
|
|||||||
<Title order={4} mb="md">Grafik Penjualan per Produk</Title>
|
<Title order={4} mb="md">Grafik Penjualan per Produk</Title>
|
||||||
<Box style={{ height: 350 }}>
|
<Box style={{ height: 350 }}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={detail.map(item => ({
|
<BarChart data={detail.map((item: any) => ({
|
||||||
name: item.namaProduk,
|
name: item.namaProduk,
|
||||||
penjualan: item.penjualanBulanIni
|
penjualan: item.penjualanBulanIni
|
||||||
}))}>
|
}))}>
|
||||||
@@ -76,16 +110,21 @@ function UmkmDashboard() {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||||
<Card h={"100%"} withBorder radius="md" p="lg" shadow="sm">
|
<Card h="100%" withBorder radius="md" p="lg" shadow="sm">
|
||||||
<Title order={4} mb="md">Top 3 Produk</Title>
|
<Title order={4} mb="md">Top 3 Produk Terlaris</Title>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{topProduk.map((item, i) => (
|
{topProduk.map((item: any, i: number) => (
|
||||||
<Group key={i} justify="space-between">
|
<Group key={i} justify="space-between" wrap="nowrap">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={500}>{item.namaProduk}</Text>
|
<Text fw={500} size="sm">{item.namaProduk}</Text>
|
||||||
<Text size="xs" c="dimmed">{item.namaUmkm}</Text>
|
<Text size="xs" c="dimmed">{item.namaUmkm}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Rp {item.totalPenjualan.toLocaleString()} · {item.jumlahTerjual} terjual
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text fw={600} c="blue">Rp {item.totalPenjualan.toLocaleString()}</Text>
|
<Badge color={item.growth >= 0 ? 'teal' : 'red'} variant="light" size="sm">
|
||||||
|
{item.growth >= 0 ? '+' : ''}{item.growth}%
|
||||||
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -94,24 +133,62 @@ function UmkmDashboard() {
|
|||||||
|
|
||||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||||
<Card withBorder radius="md" p="lg" shadow="sm">
|
<Card withBorder radius="md" p="lg" shadow="sm">
|
||||||
<Title order={4} mb="md">Detail Penjualan & Stok</Title>
|
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||||
|
<Title order={4}>Detail Penjualan Produk</Title>
|
||||||
|
<Group gap="sm">
|
||||||
|
<Select
|
||||||
|
placeholder="Semua Kategori"
|
||||||
|
data={kategoriList}
|
||||||
|
value={kategoriId || null}
|
||||||
|
onChange={(v) => setKategoriId(v || "")}
|
||||||
|
clearable
|
||||||
|
size="xs"
|
||||||
|
style={{ minWidth: 140 }}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Semua UMKM"
|
||||||
|
data={umkmList}
|
||||||
|
value={umkmId || null}
|
||||||
|
onChange={(v) => setUmkmId(v || "")}
|
||||||
|
clearable
|
||||||
|
size="xs"
|
||||||
|
style={{ minWidth: 140 }}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Produk</TableTh>
|
<TableTh>Produk</TableTh>
|
||||||
<TableTh>Penjualan</TableTh>
|
<TableTh>Penjualan Bulan Ini</TableTh>
|
||||||
|
<TableTh>Bulan Lalu</TableTh>
|
||||||
<TableTh>Trend</TableTh>
|
<TableTh>Trend</TableTh>
|
||||||
|
<TableTh>Volume</TableTh>
|
||||||
<TableTh>Stok</TableTh>
|
<TableTh>Stok</TableTh>
|
||||||
<TableTh>Status</TableTh>
|
<TableTh>Status</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{detail.map((item, i) => (
|
{detail.map((item: any, i: number) => (
|
||||||
<TableTr key={i}>
|
<TableTr key={i}>
|
||||||
<TableTd>{item.namaProduk}</TableTd>
|
<TableTd>{item.namaProduk}</TableTd>
|
||||||
<TableTd>Rp {item.penjualanBulanIni.toLocaleString()}</TableTd>
|
<TableTd>Rp {item.penjualanBulanIni.toLocaleString()}</TableTd>
|
||||||
<TableTd>{renderTrend(item.trend)}</TableTd>
|
<TableTd>Rp {item.penjualanBulanLalu.toLocaleString()}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
{renderTrend(item.trend)}
|
||||||
|
{item.trendPersen !== 0 && (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c={item.trend === 'up' ? 'green' : item.trend === 'down' ? 'red' : 'dimmed'}
|
||||||
|
>
|
||||||
|
{item.trendPersen > 0 ? '+' : ''}{item.trendPersen}%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.volume}</TableTd>
|
||||||
<TableTd>{item.stok}</TableTd>
|
<TableTd>{item.stok}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Badge color={getStatusColor(item.statusStok)}>
|
<Badge color={getStatusColor(item.statusStok)}>
|
||||||
|
|||||||
@@ -1,49 +1,86 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
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) {
|
async function umkmDashboardDetailPenjualan(context: Context) {
|
||||||
const periode = (context.query.periode as string) ||
|
const mode = context.query.mode as string | undefined;
|
||||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
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");
|
const date = new Date(periode + "-01");
|
||||||
date.setMonth(date.getMonth() - 1);
|
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 {
|
try {
|
||||||
// Ambil semua produk yang punya penjualan bulan ini atau bulan lalu
|
|
||||||
const [produkSkrg, produkLalu, allProduks] = await Promise.all([
|
const [produkSkrg, produkLalu, allProduks] = await Promise.all([
|
||||||
prisma.penjualanProduk.groupBy({
|
prisma.penjualanProduk.groupBy({
|
||||||
by: ['produkId'],
|
by: ["produkId"],
|
||||||
where: { periode, deletedAt: null },
|
where: whereSkrg,
|
||||||
_sum: { totalNilai: true, jumlah: true }
|
_sum: { totalNilai: true, jumlah: true },
|
||||||
}),
|
}),
|
||||||
prisma.penjualanProduk.groupBy({
|
prisma.penjualanProduk.groupBy({
|
||||||
by: ['produkId'],
|
by: ["produkId"],
|
||||||
where: { periode: periodeLalu, deletedAt: null },
|
where: whereLalu,
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true },
|
||||||
}),
|
}),
|
||||||
// Use PasarDesa
|
|
||||||
prisma.pasarDesa.findMany({
|
prisma.pasarDesa.findMany({
|
||||||
where: { deletedAt: null },
|
where: produkFilter,
|
||||||
select: { id: true, nama: true, stok: true }
|
select: { id: true, nama: true, stok: true },
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const data = allProduks.map(p => {
|
const data = allProduks.map((p) => {
|
||||||
const skrgRaw = produkSkrg.find(s => s.produkId === p.id)?._sum || { totalNilai: 0, jumlah: 0 };
|
const skrgRaw = produkSkrg.find((s) => s.produkId === p.id)?._sum || {};
|
||||||
const laluRaw = produkLalu.find(l => l.produkId === p.id)?._sum || { totalNilai: 0 };
|
const laluRaw = produkLalu.find((l) => l.produkId === p.id)?._sum || {};
|
||||||
|
|
||||||
const skrg = {
|
const nilaiSkrg = (skrgRaw as any).totalNilai || 0;
|
||||||
totalNilai: (skrgRaw as any).totalNilai || 0,
|
const nilaiLalu = (laluRaw as any).totalNilai || 0;
|
||||||
jumlah: (skrgRaw as any).jumlah || 0
|
const jumlah = (skrgRaw as any).jumlah || 0;
|
||||||
};
|
|
||||||
const lalu = {
|
|
||||||
totalNilai: (laluRaw as any).totalNilai || 0
|
|
||||||
};
|
|
||||||
|
|
||||||
let trend = "stable";
|
let trend = "stable";
|
||||||
if (skrg.totalNilai > lalu.totalNilai) trend = "up";
|
if (nilaiSkrg > nilaiLalu) trend = "up";
|
||||||
if (skrg.totalNilai < lalu.totalNilai) trend = "down";
|
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";
|
let statusStok = "Aman";
|
||||||
if (p.stok < 5) statusStok = "Rendah";
|
if (p.stok < 5) statusStok = "Rendah";
|
||||||
@@ -51,19 +88,17 @@ async function umkmDashboardDetailPenjualan(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
namaProduk: p.nama,
|
namaProduk: p.nama,
|
||||||
penjualanBulanIni: skrg.totalNilai,
|
penjualanBulanIni: nilaiSkrg,
|
||||||
penjualanBulanLalu: lalu.totalNilai,
|
penjualanBulanLalu: nilaiLalu,
|
||||||
trend,
|
trend,
|
||||||
volume: skrg.jumlah,
|
trendPersen,
|
||||||
|
volume: jumlah,
|
||||||
stok: p.stok,
|
stok: p.stok,
|
||||||
statusStok
|
statusStok,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { success: true, data };
|
||||||
success: true,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error di umkmDashboardDetailPenjualan:", e);
|
console.error("Error di umkmDashboardDetailPenjualan:", e);
|
||||||
return { success: false, message: "Gagal mengambil detail penjualan dashboard" };
|
return { success: false, message: "Gagal mengambil detail penjualan dashboard" };
|
||||||
|
|||||||
@@ -1,44 +1,55 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
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) {
|
async function umkmDashboardKpi(context: Context) {
|
||||||
|
const mode = context.query.mode as string | undefined;
|
||||||
|
const isWeek = mode === "week";
|
||||||
|
|
||||||
const periode =
|
const periode =
|
||||||
(context.query.periode as string) ||
|
(context.query.periode as string) ||
|
||||||
`${new Date().getFullYear()}-${String(
|
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||||
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 {
|
try {
|
||||||
// KPI utama
|
|
||||||
const [umkmAktif, totalUmkm, omzetBulanan] = await Promise.all([
|
const [umkmAktif, totalUmkm, omzetBulanan] = await Promise.all([
|
||||||
prisma.umkm.count({
|
prisma.umkm.count({ where: { isActive: true, deletedAt: null } }),
|
||||||
where: { isActive: true, deletedAt: null },
|
prisma.umkm.count({ where: { deletedAt: null } }),
|
||||||
}),
|
|
||||||
prisma.umkm.count({
|
|
||||||
where: { deletedAt: null },
|
|
||||||
}),
|
|
||||||
prisma.penjualanProduk.aggregate({
|
prisma.penjualanProduk.aggregate({
|
||||||
where: { periode, deletedAt: null },
|
where: whereClause,
|
||||||
_sum: { totalNilai: true },
|
_sum: { totalNilai: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// =========================
|
// Cari kategori dengan penjualan terbanyak
|
||||||
// 1. Cari kategori dari penjualan
|
|
||||||
// =========================
|
|
||||||
const salesByCategory = await prisma.penjualanProduk.findMany({
|
const salesByCategory = await prisma.penjualanProduk.findMany({
|
||||||
where: { periode, deletedAt: null },
|
where: whereClause,
|
||||||
select: {
|
select: {
|
||||||
jumlah: true,
|
jumlah: true,
|
||||||
produk: {
|
produk: { select: { kategoriProdukId: true } },
|
||||||
select: {
|
|
||||||
kategoriProdukId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let kategoriNama = "-";
|
let kategoriNama = "-";
|
||||||
|
let topCategoryId: string | null = null;
|
||||||
|
|
||||||
if (salesByCategory.length > 0) {
|
if (salesByCategory.length > 0) {
|
||||||
const categoryCounts: Record<string, number> = {};
|
const categoryCounts: Record<string, number> = {};
|
||||||
@@ -46,15 +57,10 @@ async function umkmDashboardKpi(context: Context) {
|
|||||||
for (const sale of salesByCategory) {
|
for (const sale of salesByCategory) {
|
||||||
const catId = sale.produk.kategoriProdukId;
|
const catId = sale.produk.kategoriProdukId;
|
||||||
if (!catId) continue;
|
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;
|
let maxSales = 0;
|
||||||
|
|
||||||
for (const [id, count] of Object.entries(categoryCounts)) {
|
for (const [id, count] of Object.entries(categoryCounts)) {
|
||||||
if (count > maxSales) {
|
if (count > maxSales) {
|
||||||
maxSales = count;
|
maxSales = count;
|
||||||
@@ -63,38 +69,40 @@ async function umkmDashboardKpi(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (topCategoryId) {
|
if (topCategoryId) {
|
||||||
const kategori = await prisma.kategoriProduk.findUnique({
|
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
||||||
where: { id: topCategoryId },
|
where: { id: topCategoryId },
|
||||||
select: { nama: true },
|
select: { nama: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
kategoriNama = kategori?.nama || "-";
|
kategoriNama = kategori?.nama || "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// Fallback: kategori dari UMKM terbanyak
|
||||||
// 2. Fallback (kalau tidak ada penjualan)
|
|
||||||
// =========================
|
|
||||||
if (kategoriNama === "-") {
|
if (kategoriNama === "-") {
|
||||||
const kategoriTerbanyakUmkm = await prisma.umkm.groupBy({
|
const kategoriTerbanyakUmkm = await prisma.umkm.groupBy({
|
||||||
by: ["kategoriId"],
|
by: ["kategoriId"],
|
||||||
_count: { _all: true },
|
_count: { _all: true },
|
||||||
orderBy: {
|
orderBy: { _count: { kategoriId: "desc" } },
|
||||||
_count: { kategoriId: "desc" },
|
|
||||||
},
|
|
||||||
take: 1,
|
take: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (kategoriTerbanyakUmkm.length > 0) {
|
if (kategoriTerbanyakUmkm.length > 0) {
|
||||||
|
topCategoryId = kategoriTerbanyakUmkm[0].kategoriId;
|
||||||
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
||||||
where: { id: kategoriTerbanyakUmkm[0].kategoriId },
|
where: { id: topCategoryId },
|
||||||
select: { nama: true },
|
select: { nama: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
kategoriNama = kategori?.nama || "-";
|
kategoriNama = kategori?.nama || "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hitung jumlah produk dalam kategori terbanyak
|
||||||
|
const jumlahKategoriTerbanyak = topCategoryId
|
||||||
|
? await prisma.pasarDesa.count({
|
||||||
|
where: { kategoriProdukId: topCategoryId, deletedAt: null },
|
||||||
|
})
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -102,14 +110,12 @@ async function umkmDashboardKpi(context: Context) {
|
|||||||
totalUmkm,
|
totalUmkm,
|
||||||
omzetBulanan: omzetBulanan._sum.totalNilai || 0,
|
omzetBulanan: omzetBulanan._sum.totalNilai || 0,
|
||||||
kategoriTerbanyak: kategoriNama,
|
kategoriTerbanyak: kategoriNama,
|
||||||
|
jumlahKategoriTerbanyak,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error di umkmDashboardKpi:", e);
|
console.error("Error di umkmDashboardKpi:", e);
|
||||||
return {
|
return { success: false, message: "Gagal mengambil data KPI dashboard" };
|
||||||
success: false,
|
|
||||||
message: "Gagal mengambil data KPI dashboard",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,58 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
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) {
|
async function umkmDashboardRingSummary(context: Context) {
|
||||||
const periode = (context.query.periode as string) ||
|
const mode = context.query.mode as string | undefined;
|
||||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
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
|
// Hitung periode bulan lalu
|
||||||
const date = new Date(periode + "-01");
|
const date = new Date(periode + "-01");
|
||||||
date.setMonth(date.getMonth() - 1);
|
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 {
|
try {
|
||||||
const [penjualanSkrg, penjualanLalu, produkAktif, totalTransaksi] = await Promise.all([
|
const [penjualanSkrg, penjualanLalu, kategoriAktif, totalTransaksi] = await Promise.all([
|
||||||
prisma.penjualanProduk.aggregate({
|
prisma.penjualanProduk.aggregate({
|
||||||
where: { periode, deletedAt: null },
|
where: whereSkrg,
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true },
|
||||||
}),
|
}),
|
||||||
prisma.penjualanProduk.aggregate({
|
prisma.penjualanProduk.aggregate({
|
||||||
where: { periode: periodeLalu, deletedAt: null },
|
where: whereLalu,
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true },
|
||||||
}),
|
}),
|
||||||
// Count from PasarDesa
|
// Hitung jumlah kategori aktif
|
||||||
prisma.pasarDesa.count({
|
prisma.kategoriProdukUmkm.count({
|
||||||
where: { isActive: true, deletedAt: null }
|
where: { isActive: true, deletedAt: null },
|
||||||
}),
|
}),
|
||||||
prisma.penjualanProduk.count({ where: { periode, deletedAt: null } })
|
prisma.penjualanProduk.count({ where: whereSkrg }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const skrg = penjualanSkrg._sum.totalNilai || 0;
|
const skrg = penjualanSkrg._sum.totalNilai || 0;
|
||||||
@@ -42,9 +70,9 @@ async function umkmDashboardRingSummary(context: Context) {
|
|||||||
data: {
|
data: {
|
||||||
totalPenjualan: skrg,
|
totalPenjualan: skrg,
|
||||||
persentasePerubahan: Math.round(persentasePerubahan * 100) / 100,
|
persentasePerubahan: Math.round(persentasePerubahan * 100) / 100,
|
||||||
produkAktif,
|
kategoriAktif,
|
||||||
totalTransaksi
|
totalTransaksi,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error di umkmDashboardRingSummary:", e);
|
console.error("Error di umkmDashboardRingSummary:", e);
|
||||||
|
|||||||
@@ -1,39 +1,91 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
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) {
|
async function umkmDashboardTopProduk(context: Context) {
|
||||||
const periode = (context.query.periode as string) ||
|
const mode = context.query.mode as string | undefined;
|
||||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
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 {
|
try {
|
||||||
const topPenjualan = await prisma.penjualanProduk.groupBy({
|
const topPenjualan = await prisma.penjualanProduk.groupBy({
|
||||||
by: ['produkId'],
|
by: ["produkId"],
|
||||||
where: { periode, deletedAt: null },
|
where: whereSkrg,
|
||||||
_sum: { totalNilai: true, jumlah: true },
|
_sum: { totalNilai: true, jumlah: true },
|
||||||
orderBy: { _sum: { totalNilai: 'desc' } },
|
orderBy: { _sum: { totalNilai: "desc" } },
|
||||||
take: 3
|
take: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await Promise.all(topPenjualan.map(async (item) => {
|
// Ambil penjualan periode lalu untuk produk yang sama
|
||||||
// Find from PasarDesa now
|
const produkIds = topPenjualan.map((p) => p.produkId);
|
||||||
|
const penjualanLalu = await prisma.penjualanProduk.groupBy({
|
||||||
|
by: ["produkId"],
|
||||||
|
where: { ...whereLalu, produkId: { in: produkIds } },
|
||||||
|
_sum: { totalNilai: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const laluMap = new Map(
|
||||||
|
penjualanLalu.map((p) => [p.produkId, p._sum.totalNilai || 0])
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await Promise.all(
|
||||||
|
topPenjualan.map(async (item) => {
|
||||||
const produk = await prisma.pasarDesa.findUnique({
|
const produk = await prisma.pasarDesa.findUnique({
|
||||||
where: { id: item.produkId },
|
where: { id: item.produkId },
|
||||||
include: { umkm: true }
|
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 {
|
return {
|
||||||
namaProduk: produk?.nama || "Unknown",
|
namaProduk: produk?.nama || "Unknown",
|
||||||
namaUmkm: produk?.umkm?.nama || "Unknown",
|
namaUmkm: produk?.umkm?.nama || "Unknown",
|
||||||
totalPenjualan: item._sum.totalNilai || 0,
|
totalPenjualan: totalSkrg,
|
||||||
jumlahTerjual: item._sum.jumlah || 0,
|
jumlahTerjual: item._sum.jumlah || 0,
|
||||||
growth: 0
|
growth,
|
||||||
};
|
};
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return { success: true, data };
|
||||||
success: true,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error di umkmDashboardTopProduk:", e);
|
console.error("Error di umkmDashboardTopProduk:", e);
|
||||||
return { success: false, message: "Gagal mengambil top produk" };
|
return { success: false, message: "Gagal mengambil top produk" };
|
||||||
|
|||||||
@@ -174,7 +174,10 @@ function DetailProdukPasarUser() {
|
|||||||
{data.stok > 0 ? `Stok: ${data.stok}` : 'Stok Habis'}
|
{data.stok > 0 ? `Stok: ${data.stok}` : 'Stok Habis'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Text>{data.deskripsi}</Text>
|
<Text
|
||||||
|
style={{ wordBreak: 'break-word' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconShoppingCart />}
|
leftSection={<IconShoppingCart />}
|
||||||
|
|||||||
Reference in New Issue
Block a user