nico/27-okt-25 #1

Merged
nicoarya20 merged 277 commits from nico/27-okt-25 into main 2025-10-27 22:18:01 +08:00
891 changed files with 68387 additions and 624 deletions
Showing only changes of commit 0fd47e3e94 - Show all commits

View File

@@ -0,0 +1,10 @@
[
{
"id": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"nama": "Makanan"
},
{
"id": "5c06chf7-123f-6hfe-0663-5e9h76e55060",
"nama": "Minuman"
}
]

View File

@@ -0,0 +1,59 @@
/*
Warnings:
- You are about to drop the column `alamat` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `kategoriId` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `satuan` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the `KategoriMakanan` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `alamatUsaha` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_imageId_fkey";
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriId_fkey";
-- AlterTable
ALTER TABLE "PasarDesa" DROP COLUMN "alamat",
DROP COLUMN "deletedAt",
DROP COLUMN "isActive",
DROP COLUMN "kategoriId",
DROP COLUMN "satuan",
ADD COLUMN "alamatUsaha" TEXT NOT NULL,
ALTER COLUMN "imageId" DROP NOT NULL;
-- DropTable
DROP TABLE "KategoriMakanan";
-- CreateTable
CREATE TABLE "KategoriProduk" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "KategoriProduk_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ProdukToKategori" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ProdukToKategori_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ProdukToKategori_B_index" ON "_ProdukToKategori"("B");
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_A_fkey" FOREIGN KEY ("A") REFERENCES "KategoriProduk"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_B_fkey" FOREIGN KEY ("B") REFERENCES "PasarDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -933,7 +933,7 @@ model LayananPolsek {
// ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan {
id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan"
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan"
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kontakItems KontakItem[]
@@ -942,15 +942,15 @@ model KontakDaruratKeamanan {
}
model KontakItem {
id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba"
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba"
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
@@ -1050,68 +1050,67 @@ model MenuTipsKeamanan {
// ========================================= MENU EKONOMI ========================================= //
// ========================================= PASAR DESA ========================================= //
model PasarDesa {
id String @id @default(uuid())
nama String // contoh: "Kerupuk Babi"
harga Int // disimpan dalam bentuk angka: 12000
satuan String // contoh: "pcs", "1 kg"
alamat String // contoh: "Jl. Kenari no.7"
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
rating Float // contoh: 4.9
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
kategori KategoriMakanan @relation(fields: [kategoriId], references: [id])
kategoriId String
id String @id @default(uuid())
nama String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
harga Int
rating Float
alamatUsaha String
kategori KategoriProduk[] @relation("ProdukToKategori")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model KategoriMakanan {
id String @id @default(uuid())
nama String // contoh: "Makanan", "Bahan Bangunan", dll
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
PasarDesa PasarDesa[]
model KategoriProduk {
id String @id @default(uuid())
nama String
produk PasarDesa[] @relation("ProdukToKategori")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LOWONGAN KERJA LOKAL ========================================= //
model LowonganPekerjaan {
id String @id @default(uuid()) // ID unik untuk setiap lowongan
posisi String // Contoh: "Kasir"
namaPerusahaan String // Contoh: "Toko Sumber Rejeki"
lokasi String // Contoh: "Desa Munggu , Badung"
tipePekerjaan String // Contoh: "Full Time", "Part Time", "Contract"
gaji String // Contoh: "Rp. 2.500.000 / bulan". Menggunakan String karena formatnya bisa bervariasi
deskripsi String // Opsional: Detail deskripsi pekerjaan (tidak terlihat di UI ini, tapi umum ada)
kualifikasi String // Opsional: Kualifikasi yang dibutuhkan (tidak terlihat di UI ini, tapi umum ada)
id String @id @default(uuid()) // ID unik untuk setiap lowongan
posisi String // Contoh: "Kasir"
namaPerusahaan String // Contoh: "Toko Sumber Rejeki"
lokasi String // Contoh: "Desa Munggu , Badung"
tipePekerjaan String // Contoh: "Full Time", "Part Time", "Contract"
gaji String // Contoh: "Rp. 2.500.000 / bulan". Menggunakan String karena formatnya bisa bervariasi
deskripsi String // Opsional: Detail deskripsi pekerjaan (tidak terlihat di UI ini, tapi umum ada)
kualifikasi String // Opsional: Kualifikasi yang dibutuhkan (tidak terlihat di UI ini, tapi umum ada)
tanggalPosting DateTime @default(now()) // Tanggal lowongan diposting
isActive Boolean @default(true) // Menandakan apakah lowongan masih aktif
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true) // Menandakan apakah lowongan masih aktif
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
model ProgramKemiskinan {
id String @id @default(uuid())
id String @id @default(uuid())
nama String
deskripsi String
ikonUrl String?
isActive Boolean @default(true)
isActive Boolean @default(true)
// Tambahkan relasi one-to-one ke StatistikKemiskinan
statistikId String? @unique // Foreign key ke StatistikKemiskinan, unique untuk one-to-one
statistikId String? @unique // Foreign key ke StatistikKemiskinan, unique untuk one-to-one
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model StatistikKemiskinan {
id String @id @default(uuid())
tahun Int @unique
jumlah Int
id String @id @default(uuid())
tahun Int @unique
jumlah Int
// Tidak perlu foreign key di sini jika relasi di ProgramLayanan
programKemiskinan ProgramKemiskinan?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

Binary file not shown.

View File

@@ -17,6 +17,7 @@ import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import lambangDesa from "./data/desa/profile/lambang_desa.json";
import maskotDesa from "./data/desa/profile/maskot_desa.json";
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
(async () => {
for (const l of layanan) {
@@ -340,6 +341,22 @@ import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
});
}
console.log("dasar hukum PPID success ...");
for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("kategori produk success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -4,32 +4,30 @@ import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(3, "Nama minimal 3 karakter"),
const templatePasarDesaForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
harga: z.number().min(1, "Harga minimal 1"),
satuan: z.string().min(3, "Satuan minimal 3 karakter"),
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
alamatUsaha: z.string().min(1, "Alamat minimal 1 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.string().min(1, "Kategori wajib dipilih"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
});
const defaultForm = {
const defaultPasarDesaForm = {
nama: "",
harga: 0,
satuan: "",
alamat: "",
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: "",
kategoriId: [] as string[],
};
const pasarDesaState = proxy({
const pasarDesa = proxy({
create: {
form: { ...defaultForm },
form: { ...defaultPasarDesaForm },
loading: false,
async create() {
const cek = templateForm.safeParse(pasarDesaState.create.form);
const cek = templatePasarDesaForm.safeParse(pasarDesa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -37,12 +35,12 @@ const pasarDesaState = proxy({
return toast.error(err);
}
try {
pasarDesaState.create.loading = true;
pasarDesa.create.loading = true;
const res = await ApiFetch.api.ekonomi.pasardesa["create"].post(
pasarDesaState.create.form
pasarDesa.create.form
);
if (res.status === 200) {
pasarDesaState.findMany.load();
pasarDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
@@ -50,7 +48,7 @@ const pasarDesaState = proxy({
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pasarDesaState.create.loading = false;
pasarDesa.create.loading = false;
}
},
},
@@ -63,7 +61,7 @@ const pasarDesaState = proxy({
async load() {
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get();
if (res.status === 200) {
pasarDesaState.findMany.data = res.data?.data ?? [];
pasarDesa.findMany.data = res.data?.data ?? [];
}
},
},
@@ -76,14 +74,14 @@ const pasarDesaState = proxy({
const res = await fetch(`/api/ekonomi/pasardesa/${id}`);
if (res.ok) {
const data = await res.json();
pasarDesaState.findUnique.data = data.data ?? null;
pasarDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pasarDesaState.findUnique.data = null;
pasarDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pasarDesaState.findUnique.data = null;
pasarDesa.findUnique.data = null;
}
},
},
@@ -93,7 +91,7 @@ const pasarDesaState = proxy({
if (!id) return toast.warn("ID tidak valid");
try {
pasarDesaState.delete.loading = true;
pasarDesa.delete.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/del/${id}`, {
method: "DELETE",
@@ -106,7 +104,7 @@ const pasarDesaState = proxy({
if (response.ok && result?.success) {
toast.success(result.message || "Pasar desa berhasil dihapus");
await pasarDesaState.findMany.load(); // refresh list
await pasarDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pasar desa");
}
@@ -114,13 +112,218 @@ const pasarDesaState = proxy({
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pasar desa");
} finally {
pasarDesaState.delete.loading = false;
pasarDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultPasarDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pasardesa/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
harga: data.harga,
alamatUsaha: data.alamatUsaha,
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePasarDesaForm.safeParse(pasarDesa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pasarDesa.edit.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
harga: this.form.harga,
alamatUsaha: this.form.alamatUsaha,
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pasar desa");
await pasarDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pasar desa");
}
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pasar desa"
);
return false;
} finally {
pasarDesa.edit.loading = false;
}
},
reset() {
pasarDesa.edit.id = "";
pasarDesa.edit.form = { ...defaultPasarDesaForm };
},
},
});
// ========================================= KATEGORI PRODUK ========================================= //
const kategoriProdukForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriProdukDefaultForm = {
nama: "",
};
const kategoriProduk = proxy({
create: {
form: { ...kategoriProdukDefaultForm },
loading: false,
async create() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriProduk.create.loading = true;
const res = await ApiFetch.api.ekonomi.kategoriproduk["create"].post(
kategoriProduk.create.form
);
if (res.status === 200) {
kategoriProduk.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriProduk.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}>
| null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
kategoriProduk.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`);
if (res.ok) {
const data = await res.json();
kategoriProduk.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriProduk.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriProduk.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriProduk.delete.loading = true;
const response = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori produk berhasil dihapus");
await kategoriProduk.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori produk");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori produk");
} finally {
kategoriProduk.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
@@ -130,7 +333,7 @@ const pasarDesaState = proxy({
}
try {
const response = await fetch(`/api/ekonomi/pasardesa/${id}`, {
const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -145,19 +348,13 @@ const pasarDesaState = proxy({
this.id = data.id;
this.form = {
nama: data.nama,
harga: data.harga,
satuan: data.satuan,
alamat: data.alamat,
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pasar desa:", error);
console.error("Error loading kategori produk:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
@@ -166,65 +363,83 @@ const pasarDesaState = proxy({
},
async update() {
const cek = templateForm.safeParse(pasarDesaState.edit.form);
const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
toast.error(err);
return false;
}
try {
pasarDesaState.edit.loading = true;
kategoriProduk.edit.loading = true;
const response = await fetch(
`/api/ekonomi/pasardesa/${this.id}`,
`/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
harga: this.form.harga,
satuan: this.form.satuan,
alamat: this.form.alamat,
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
nama: kategoriProduk.edit.form.nama,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pasar desa");
await pasarDesaState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pasar desa");
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error('Update failed with status:', response.status, 'Response:', result);
throw new Error(
result?.message || `Gagal mengupdate kategori produk (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui kategori produk");
await kategoriProduk.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kategori produk");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error('Error response text:', text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error('Error parsing response as text:', textError);
console.error('Original error:', error);
throw new Error('Gagal memproses respons dari server');
}
}
} catch (error) {
console.error("Error updating pasar desa:", error);
console.error("Error updating kategori produk:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate pasar desa"
: "Gagal mengupdate kategori produk"
);
return false;
} finally {
pasarDesaState.edit.loading = false;
kategoriProduk.edit.loading = false;
}
},
reset() {
pasarDesaState.edit.id = "";
pasarDesaState.edit.form = { ...defaultForm };
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
},
});
const pasarDesaState = proxy({
pasarDesa,
kategoriProduk
});
export default pasarDesaState;

View File

@@ -12,12 +12,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
{
label: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/pasar-desa-ui"
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
label: "Kategori Makanan",
value: "kategorimakanan",
href: "/admin/ekonomi/pasar-desa/kategori-makanan"
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
function EditKategoriProduk() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadKategoriProduk = async () => {
if (!id) return;
try {
const data = await statePasar.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
statePasar.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error("Gagal memuat data kategori produk");
}
};
loadKategoriProduk();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama kategori produk tidak boleh kosong');
return;
}
statePasar.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
const success = await statePasar.edit.update();
if (success) {
router.push("/admin/ekonomi/pasar-desa/kategori-produk");
}
} catch (error) {
console.error("Error updating kategori produk:", error);
// toast akan ditampilkan dari fungsi update
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kategori Produk</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriProduk;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateKategoriProduk() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk)
useEffect(() => {
statePasar.findMany.load();
}, []);
const resetForm = () => {
statePasar.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/kategori-produk")
}
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Produk</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKategoriProduk;

View File

@@ -1,19 +1,21 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function PasarDesa() {
return (
<Box>
<HeaderSearch
title='Kategori Makanan'
title='Kategori Produk'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
@@ -23,13 +25,24 @@ function PasarDesa() {
}
function ListPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
// const params = useParams()
const router = useRouter()
useShallowEffect(() => {
statePasar.findMany.load()
}, [])
const handleHapus = () => {
if (selectedId) {
statePasar.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!statePasar.findMany.data) {
return (
<Stack py={10}>
@@ -43,28 +56,31 @@ function ListPasarDesa() {
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/create'
href='/admin/ekonomi/pasar-desa/kategori-produk/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{statePasar.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.harga}</TableTd>
<TableTd>{item.rating}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/${item.id}`)}>
<IconDeviceImac size={20} />
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
@@ -72,6 +88,13 @@ function ListPasarDesa() {
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori produk ini?'
/>
</Box>
);
}

View File

@@ -1,56 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../../keamanan/_com/keamananEditor';
function CreatePasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -1,74 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp. 20.000</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>5</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>Jalan In Aja</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/pasar-desa/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPasarDesa;

View File

@@ -1,56 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../../keamanan/_com/keamananEditor';
function EditPasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,223 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || "",
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || "",
imageId: pasarState.pasarDesa.edit.form.imageId || "",
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
})
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pasarState.pasarDesa.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
harga: data.harga || 0,
alamatUsaha: data.alamatUsaha || "",
imageId: data.imageId || "",
rating: data.rating || 0,
kategoriId: data.kategori?.map((k: any) => k.nama) || [],
});
// Tampilkan preview gambar
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pasar desa"
);
}
}
loadPasarDesa();
}, [params?.id]);
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = {
...pasarState.pasarDesa.edit.form,
nama: formData.nama,
harga: formData.harga,
alamatUsaha: formData.alamatUsaha,
imageId: formData.imageId,
rating: formData.rating,
kategoriId: formData.kategoriId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
}
await pasarState.pasarDesa.edit.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error("Terjadi kesalahan saat memperbarui pasar desa");
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={formData.kategoriId}
onChange={(val) => {
setFormData({ ...formData, kategoriId: val });
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,
label: v.nama
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,127 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
statePasar.kategoriProduk.findUnique.load(params?.id as string)
statePasar.pasarDesa.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePasar.pasarDesa.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
}
if (!statePasar.pasarDesa.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp.{statePasar.pasarDesa.findUnique.data?.harga}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.rating}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.alamatUsaha}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePasar.pasarDesa.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Stack gap={4}>
{statePasar.pasarDesa.findUnique.data?.kategori?.length > 0 ? (
statePasar.pasarDesa.findUnique.data.kategori.map((kat) => (
<Text fz={"lg"} key={kat.id}> {kat.nama}</Text>
))
) : (
<Text fz={"lg"} c="dimmed">Tidak ada kategori</Text>
)}
</Stack>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
setSelectedId(statePasar.pasarDesa.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${statePasar.pasarDesa.findUnique.data.id}/edit`);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus produk ini?"
/>
</Box>
);
}
export default DetailPasarDesa;

View File

@@ -0,0 +1,192 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreatePasarDesa() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
statePasar.kategoriProduk.findMany.load();
}, []);
const resetForm = () => {
statePasar.pasarDesa.create.form = {
nama: "",
harga: 0,
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: [],
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={statePasar.pasarDesa.create.form.nama}
onChange={(val) => {
statePasar.pasarDesa.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={statePasar.pasarDesa.create.form.harga}
onChange={(val) => {
statePasar.pasarDesa.create.form.harga = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={statePasar.pasarDesa.create.form.rating}
onChange={(val) => {
const value = Number(val.target.value);
// Validasi manual juga boleh (jaga-jaga)
if (value >= 0 && value <= 5) {
statePasar.pasarDesa.create.form.rating = value;
}
}}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={statePasar.pasarDesa.create.form.alamatUsaha}
onChange={(val) => {
statePasar.pasarDesa.create.form.alamatUsaha = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => {
statePasar.pasarDesa.create.form.kategoriId = val;
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,
label: v.nama
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -23,7 +23,7 @@ function PasarDesa() {
}
function ListPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const statePasar = useProxy(pasarDesaState.pasarDesa)
const router = useRouter();
useShallowEffect(() => {
@@ -43,7 +43,7 @@ function ListPasarDesa() {
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/create'
href='/admin/ekonomi/pasar-desa/produk-pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
@@ -59,11 +59,11 @@ function ListPasarDesa() {
{statePasar.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.harga}</TableTd>
<TableTd>Rp.{item.harga}</TableTd>
<TableTd>{item.rating}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>{item.alamatUsaha}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/${item.id}`)}>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>

View File

@@ -215,7 +215,7 @@ export const navBar = [
{
id: "Ekonomi_1",
name: "Pasar Desa",
path: "/admin/ekonomi/pasar-desa/pasar-desa-ui"
path: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
id: "Ekonomi_2",

View File

@@ -1,15 +1,15 @@
import Elysia from "elysia";
import PasarDesa from "./pasar-desa";
import KategoriMakanan from "./kategori-makanan";
import LowonganKerja from "./lowongan-kerja";
import ProgramKemiskinan from "./program-kemiskinan";
import KategoriProduk from "./pasar-desa/kategori-produk";
const Ekonomi = new Elysia({
prefix: "/api/ekonomi",
tags: ["Ekonomi"],
})
.use(PasarDesa)
.use(KategoriMakanan)
.use(KategoriProduk)
.use(LowonganKerja)
.use(ProgramKemiskinan)

View File

@@ -1,30 +0,0 @@
import Elysia from "elysia";
import kategoriMakananFindMany from "./findMany";
import kategoriMakananFindUnique from "./findUnique";
import kategoriMakananDelete from "./del";
import kategoriMakananCreate from "./create";
import kategoriMakananUpdate from "./updt";
import { t } from "elysia";
const KategoriMakanan = new Elysia({
prefix: "/kategori-makanan",
tags: ["Ekonomi/Kategori Makanan"],
})
.get("/find-many", kategoriMakananFindMany)
.get("/:id", async (context) => {
const response = await kategoriMakananFindUnique(context);
return response;
})
.delete("/del/:id", kategoriMakananDelete)
.post("/create", kategoriMakananCreate, {
body: t.Object({
nama: t.String(),
}),
})
.put("/:id", kategoriMakananUpdate, {
body: t.Object({
nama: t.String(),
}),
});
export default KategoriMakanan;

View File

@@ -1,35 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriMakananUpdate(context: Context) {
const body = await context.request.json()
if (!body.nama) {
return {
success: false,
message: "Nama is required",
}
}
const kategoriMakanan = await prisma.kategoriMakanan.update({
where: {
id: body.id,
},
data: {
nama: body.nama,
},
})
if(!kategoriMakanan) {
return {
success: false,
message: "Kategori makanan tidak ditemukan",
}
}
return {
success: true,
message: "Success update kategori makanan",
data: kategoriMakanan,
}
}

View File

@@ -4,11 +4,10 @@ import { Context } from "elysia";
type FormCreate = {
nama: string;
harga: number;
satuan: string;
alamat: string;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string; // Array of KategoriMakanan IDs
kategoriId: string[]; // Array of KategoriMakanan IDs
};
export default async function pasarDesaCreate(context: Context) {
const body = context.body as FormCreate;
@@ -18,11 +17,12 @@ export default async function pasarDesaCreate(context: Context) {
data: {
nama: body.nama,
harga: Number(body.harga),
satuan: body.satuan,
alamat: body.alamat,
alamatUsaha: body.alamatUsaha,
imageId: body.imageId,
rating: Number(body.rating),
kategoriId: body.kategoriId,
kategori: {
connect: body.kategoriId.map((id) => ({ id })),
},
},
include: {
image: true,

View File

@@ -18,11 +18,10 @@ const PasarDesa = new Elysia({
body: t.Object({
nama: t.String(),
harga: t.Number(),
satuan: t.String(),
alamat: t.String(),
alamatUsaha: t.String(),
imageId: t.String(),
rating: t.Number(),
kategoriId:t.String(),
kategoriId:t.Array(t.String()),
}),
})
.delete("/del/:id", pasarDesaDelete)
@@ -36,11 +35,10 @@ const PasarDesa = new Elysia({
body: t.Object({
nama: t.String(),
harga: t.Number(),
satuan: t.String(),
alamat: t.String(),
alamatUsaha: t.String(),
imageId: t.String(),
rating: t.Number(),
kategoriId: t.String(),
kategoriId:t.Array(t.String()),
}),
}
);

View File

@@ -2,7 +2,7 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriMakananCreate(context: Context) {
export default async function kategoriProdukCreate(context: Context) {
const body = context.body as {nama: string};
if (!body.nama) {
@@ -12,15 +12,14 @@ export default async function kategoriMakananCreate(context: Context) {
};
}
const kategoriMakanan = await prisma.kategoriMakanan.create({
const kategoriProduk = await prisma.kategoriProduk.create({
data: {
nama: body.nama,
deletedAt: null,
},
});
return {
success: true,
message: "Success create kategori makanan",
data: kategoriMakanan
message: "Success create kategori produk",
data: kategoriProduk
};
}

View File

@@ -1,7 +1,7 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const kategoriMakananDelete = async (context: Context) => {
const kategoriProdukDelete = async (context: Context) => {
const id = context.params.id;
if (!id) {
return {
@@ -10,24 +10,24 @@ const kategoriMakananDelete = async (context: Context) => {
}
}
const kategoriMakanan = await prisma.kategoriMakanan.delete({
const kategoriProduk = await prisma.kategoriProduk.delete({
where: {
id: id,
},
})
if(!kategoriMakanan) {
if(!kategoriProduk) {
return {
success: false,
message: "Kategori makanan tidak ditemukan",
message: "Kategori Produk tidak ditemukan",
}
}
return {
success: true,
message: "Success delete kategori makanan",
data: kategoriMakanan,
message: "Success delete kategori produk",
data: kategoriProduk,
}
}
export default kategoriMakananDelete
export default kategoriProdukDelete

View File

@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
export default async function kategoriMakananFindMany() {
const data = await prisma.kategoriMakanan.findMany();
export default async function kategoriProdukFindMany() {
const data = await prisma.kategoriProduk.findMany();
return {
success: true,
data: data.map((item: any) => {

View File

@@ -1,7 +1,7 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export default async function kategoriMakananFindUnique(context: Context) {
export default async function kategoriProdukFindUnique(context: Context) {
const url = new URL(context.request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
@@ -21,7 +21,7 @@ export default async function kategoriMakananFindUnique(context: Context) {
}
}
const data = await prisma.kategoriMakanan.findUnique({
const data = await prisma.kategoriProduk.findUnique({
where: { id },
});

View File

@@ -0,0 +1,30 @@
import Elysia from "elysia";
import kategoriProdukFindMany from "./findMany";
import kategoriProdukFindUnique from "./findUnique";
import kategoriProdukDelete from "./del";
import kategoriProdukCreate from "./create";
import kategoriProdukUpdate from "./updt";
import { t } from "elysia";
const KategoriProduk = new Elysia({
prefix: "/kategoriproduk",
tags: ["Ekonomi/Kategori Produk"],
})
.get("/find-many", kategoriProdukFindMany)
.get("/:id", async (context) => {
const response = await kategoriProdukFindUnique(context);
return response;
})
.delete("/del/:id", kategoriProdukDelete)
.post("/create", kategoriProdukCreate, {
body: t.Object({
nama: t.String(),
}),
})
.put("/:id", kategoriProdukUpdate, {
body: t.Object({
nama: t.String(),
}),
});
export default KategoriProduk;

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriProdukUpdate(context: Context) {
const body = context.body as { nama: string };
const id = context.params?.id as string;
// Validasi ID dan nama
if (!id) {
return {
success: false,
message: "ID is required",
};
}
if (!body.nama) {
return {
success: false,
message: "Nama is required",
};
}
try {
const kategoriProduk = await prisma.kategoriProduk.update({
where: { id },
data: {
nama: body.nama,
},
});
return {
success: true,
message: "Success update kategori produk",
data: kategoriProduk,
};
} catch (error) {
console.error("Update error:", error);
return {
success: false,
message: "Gagal update kategori produk",
error: error instanceof Error ? error.message : String(error),
};
}
}

View File

@@ -6,11 +6,10 @@ import path from "path";
type FormUpdate = {
nama: string;
harga: number;
satuan: string;
alamat: string;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string; // Array of KategoriMakanan IDs
kategoriId: string[]; // Array of KategoriMakanan IDs
};
export default async function pasarDesaUpdate(context: Context){
@@ -18,7 +17,7 @@ export default async function pasarDesaUpdate(context: Context){
const id = context.params?.id;
const body = context.body as FormUpdate;
const { nama, harga, satuan, alamat, imageId, rating, kategoriId } = body;
const { nama, harga, alamatUsaha, imageId, rating, kategoriId } = body;
if (!id) {
return Response.json({
@@ -63,11 +62,12 @@ export default async function pasarDesaUpdate(context: Context){
data: {
nama,
harga,
satuan,
alamat,
alamatUsaha,
imageId,
rating,
kategoriId,
kategori: {
connect: kategoriId.map((id) => ({ id })),
},
},
});