feat(ekonomi): refactor umkm module with sales delete, stock validation, and ordering system
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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)' }}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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)' }}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user