feat(ekonomi): refactor umkm module with sales delete, stock validation, and ordering system

This commit is contained in:
2026-04-24 16:57:43 +08:00
parent 187e3a2115
commit cd7425292c
21 changed files with 561 additions and 248 deletions

View File

@@ -8,10 +8,10 @@ const umkmFormSchema = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
pemilik: z.string().min(1, "Nama pemilik wajib diisi"),
kategoriId: z.string().min(1, "Kategori wajib dipilih"),
deskripsi: z.string().optional(),
alamat: z.string().optional(),
kontak: z.string().optional(),
imageId: z.string().optional(),
deskripsi: z.string().optional().nullable(),
alamat: z.string().optional().nullable(),
kontak: z.string().optional().nullable(),
imageId: z.string().optional().nullable(),
});
const defaultUmkmForm = {
@@ -21,7 +21,7 @@ const defaultUmkmForm = {
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
imageId: null as string | null,
isActive: true,
};
@@ -31,8 +31,8 @@ const produkFormSchema = z.object({
harga: z.number().min(0, "Harga tidak boleh negatif"),
stok: z.number().min(0, "Stok tidak boleh negatif"),
umkmId: z.string().min(1, "UMKM wajib dipilih"),
deskripsi: z.string().optional(),
imageId: z.string().optional(),
deskripsi: z.string().optional().nullable(),
imageId: z.string().optional().nullable(),
kategoriId: z.string().min(1, "Kategori wajib dipilih"), // PasarDesa needs category
});
@@ -42,7 +42,7 @@ const defaultProdukForm = {
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
imageId: null as string | null,
kategoriId: "",
isActive: true,
};
@@ -340,6 +340,29 @@ export const umkmState = proxy({
} catch (e) { toast.error("Gagal mencatat penjualan"); } finally { this.loading = false; }
return false;
}
},
del: {
loading: false,
async submit(id: string) {
this.loading = true;
try {
const res = await fetch(`/api/ekonomi/umkm/penjualan/del/${id}`, {
method: "DELETE"
});
const result = await res.json();
if (result.success) {
toast.success("Histori penjualan berhasil dihapus");
umkmState.penjualan.findMany.load();
return true;
}
toast.error(result.message);
} catch (e) {
toast.error("Gagal menghapus histori penjualan");
} finally {
this.loading = false;
}
return false;
}
}
},

View File

@@ -76,7 +76,7 @@ function UmkmDashboard() {
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card 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>
<Stack gap="sm">
{topProduk.map((item, i) => (

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
@@ -41,7 +40,7 @@ interface UmkmData {
alamat: string | null;
kontak: string | null;
imageId: string | null;
image?: { url: string } | null;
image?: { link: string } | null;
}
interface UmkmForm {
@@ -51,7 +50,7 @@ interface UmkmForm {
deskripsi: string;
alamat: string;
kontak: string;
imageId: string;
imageId: string | null;
}
function EditDataUmkm() {
@@ -71,7 +70,7 @@ function EditDataUmkm() {
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
imageId: null,
});
const [originalData, setOriginalData] = useState<UmkmForm & { imageUrl: string }>({
@@ -81,7 +80,7 @@ function EditDataUmkm() {
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
imageId: null,
imageUrl: ""
});
@@ -115,11 +114,11 @@ function EditDataUmkm() {
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
imageUrl: data.image?.link || ""
});
if (data.image?.url) {
setPreviewImage(data.image.url);
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
setIsInitialLoading(false);
@@ -281,6 +280,7 @@ function EditDataUmkm() {
onClick={() => {
setPreviewImage(null);
setFile(null);
setFormData(prev => ({ ...prev, imageId: null }));
}}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>

View File

@@ -42,7 +42,7 @@ export default function CreateDataUmkm() {
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
imageId: null,
isActive: true,
};
setPreviewImage(null);
@@ -53,7 +53,7 @@ export default function CreateDataUmkm() {
setIsSubmitting(true);
try {
// 1. Upload image first if exists
let uploadedImageId = "";
let uploadedImageId = null;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,

View File

@@ -1,4 +1,5 @@
'use client'
'use client';
import {
Box,
Button,
@@ -14,21 +15,40 @@ import {
TableTh,
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import umkmState from '../../../_state/ekonomi/umkm/umkm';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function PenjualanUmkm() {
const state = useProxy(umkmState.penjualan.findMany);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => {
state.load(state.page, 10);
}, [state.page]);
const handleHapus = async () => {
if (!selectedId) return;
const success = await umkmState.penjualan.del.submit(selectedId);
if (success) {
setModalHapus(false);
setSelectedId(null);
// 🔥 reload data
state.load(state.page, 10);
}
};
return (
<Stack gap="lg">
<Group justify="space-between">
@@ -54,16 +74,30 @@ function PenjualanUmkm() {
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{state.data.map((item) => (
<TableTr key={item.id}>
<TableTd>{new Date(item.tanggal).toLocaleDateString('id-ID')}</TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID')}
</TableTd>
<TableTd fw={500}>{item.produk?.nama}</TableTd>
<TableTd>{item.produk?.umkm?.nama}</TableTd>
<TableTd>{item.jumlah}</TableTd>
<TableTd fw={600}>Rp {item.totalNilai.toLocaleString()}</TableTd>
<TableTd fw={600}>
Rp {item.totalNilai.toLocaleString()}
</TableTd>
<TableTd>
<Button variant="subtle" color="red" size="xs">
<Button
variant="subtle"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</TableTd>
@@ -82,8 +116,16 @@ function PenjualanUmkm() {
/>
</Center>
</Paper>
{/* 🔥 Modal Konfirmasi */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data penjualan ini?"
/>
</Stack>
);
}
export default PenjualanUmkm;
export default PenjualanUmkm;

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
@@ -41,8 +40,8 @@ interface ProdukData {
umkmId: string | null;
deskripsi: string | null;
imageId: string | null;
kategoriId: string | null;
image?: { url: string } | null;
kategoriProdukId: string | null;
image?: { link: string } | null;
}
interface ProdukForm {
@@ -51,7 +50,7 @@ interface ProdukForm {
stok: number;
umkmId: string;
deskripsi: string;
imageId: string;
imageId: string | null;
kategoriId: string;
}
@@ -71,7 +70,7 @@ function EditProdukUmkm() {
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
imageId: null,
kategoriId: "",
});
@@ -81,7 +80,7 @@ function EditProdukUmkm() {
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
imageId: null,
kategoriId: "",
imageUrl: ""
});
@@ -113,17 +112,17 @@ function EditProdukUmkm() {
umkmId: data.umkmId || "",
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
kategoriId: data.kategoriId || "",
kategoriId: data.kategoriProdukId || "",
};
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
imageUrl: data.image?.link || ""
});
if (data.image?.url) {
setPreviewImage(data.image.url);
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
setIsInitialLoading(false);
@@ -285,6 +284,7 @@ function EditProdukUmkm() {
onClick={() => {
setPreviewImage(null);
setFile(null);
setFormData(prev => ({ ...prev, imageId: null }));
}}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>

View File

@@ -14,7 +14,7 @@ import {
NumberInput
} from '@mantine/core';
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconPhoto, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -43,7 +43,7 @@ export default function CreateProdukUmkm() {
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
imageId: null,
kategoriId: "",
isActive: true,
};
@@ -54,7 +54,7 @@ export default function CreateProdukUmkm() {
const handleCreate = async () => {
setIsSubmitting(true);
try {
let uploadedImageId = "";
let uploadedImageId = null;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,