UI & API Menu Landing Page, Submenu Desa Anti Korupsi
This commit is contained in:
@@ -60,6 +60,7 @@ model FileStorage {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
link String
|
link String
|
||||||
|
category String // "image" / "document" / "other"
|
||||||
Berita Berita[]
|
Berita Berita[]
|
||||||
PotensiDesa PotensiDesa[]
|
PotensiDesa PotensiDesa[]
|
||||||
Posyandu Posyandu[]
|
Posyandu Posyandu[]
|
||||||
@@ -86,28 +87,25 @@ model FileStorage {
|
|||||||
KolaborasiInovasi KolaborasiInovasi[]
|
KolaborasiInovasi KolaborasiInovasi[]
|
||||||
InfoTekno InfoTekno[]
|
InfoTekno InfoTekno[]
|
||||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||||
|
KegiatanDesa KegiatanDesa[]
|
||||||
KegiatanDesa KegiatanDesa[]
|
ProgramInovasi ProgramInovasi[]
|
||||||
|
PejabatDesa PejabatDesa[]
|
||||||
ProgramInovasi ProgramInovasi[]
|
MediaSosial MediaSosial[]
|
||||||
|
DesaAntiKorupsi DesaAntiKorupsi[]
|
||||||
PejabatDesa PejabatDesa[]
|
|
||||||
|
|
||||||
MediaSosial MediaSosial[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
//========================================= PROFILE ========================================= //
|
//========================================= PROFILE ========================================= //
|
||||||
model PejabatDesa {
|
model PejabatDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique @db.VarChar(255)
|
name String @unique @db.VarChar(255)
|
||||||
position String
|
position String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model ProgramInovasi {
|
model ProgramInovasi {
|
||||||
@@ -135,6 +133,31 @@ model MediaSosial {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//========================================= PROFILE ========================================= //
|
||||||
|
model DesaAntiKorupsi {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
deskripsi String @db.Text
|
||||||
|
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
|
||||||
|
kategoriId String
|
||||||
|
file FileStorage @relation(fields: [fileId], references: [id])
|
||||||
|
fileId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model KategoriDesaAntiKorupsi {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
DesaAntiKorupsi DesaAntiKorupsi[]
|
||||||
|
}
|
||||||
|
|
||||||
//========================================= MENU PPID ========================================= //
|
//========================================= MENU PPID ========================================= //
|
||||||
|
|
||||||
//========================================= STRUKTUR PPID ========================================= //
|
//========================================= STRUKTUR PPID ========================================= //
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const templateDesaAntiKorupsiForm = z.object({
|
||||||
|
name: z.string().min(1, "Judul minimal 1 karakter"),
|
||||||
|
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||||
|
kategoriId: z.string().min(1, "Kategori minimal 1"),
|
||||||
|
fileId: z.string().min(1, "File minimal 1"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultDesaAntiKorupsiForm = {
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
kategoriId: "",
|
||||||
|
fileId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const desaAntikorupsi = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultDesaAntiKorupsiForm },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateDesaAntiKorupsiForm.safeParse(
|
||||||
|
desaAntikorupsi.create.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
desaAntikorupsi.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||||
|
"create"
|
||||||
|
].post({
|
||||||
|
...desaAntikorupsi.create.form,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
desaAntikorupsi.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 {
|
||||||
|
desaAntikorupsi.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as Array<
|
||||||
|
Prisma.DesaAntiKorupsiGetPayload<{
|
||||||
|
include: {
|
||||||
|
file: true;
|
||||||
|
kategori: true;
|
||||||
|
};
|
||||||
|
}>
|
||||||
|
> | null,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
desaAntikorupsi.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.DesaAntiKorupsiGetPayload<{
|
||||||
|
include: {
|
||||||
|
file: true;
|
||||||
|
kategori: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
desaAntikorupsi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
desaAntikorupsi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
desaAntikorupsi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
desaAntikorupsi.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/landingpage/desaantikorupsi/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "desa anti korupsi berhasil dihapus");
|
||||||
|
await desaAntikorupsi.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus desa anti korupsi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
|
||||||
|
} finally {
|
||||||
|
desaAntikorupsi.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultDesaAntiKorupsiForm },
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
desaAntikorupsi.edit.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(`/api/landingpage/desaantikorupsi/${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 = {
|
||||||
|
name: data.name,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
kategoriId: data.kategoriId,
|
||||||
|
fileId: data.fileId,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading desa anti korupsi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
desaAntikorupsi.edit.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
const cek = templateDesaAntiKorupsiForm.safeParse(
|
||||||
|
desaAntikorupsi.edit.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
desaAntikorupsi.edit.loading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/landingpage/desaantikorupsi/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: this.form.name,
|
||||||
|
deskripsi: this.form.deskripsi,
|
||||||
|
kategoriId: this.form.kategoriId,
|
||||||
|
fileId: this.form.fileId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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 desa anti korupsi");
|
||||||
|
await desaAntikorupsi.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengupdate desa anti korupsi"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating desa anti korupsi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Gagal mengupdate desa anti korupsi"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
desaAntikorupsi.edit.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
desaAntikorupsi.edit.id = "";
|
||||||
|
desaAntikorupsi.edit.form = { ...defaultDesaAntiKorupsiForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================= KATEGORI desa anti korupsi ========================================= //
|
||||||
|
const kategoriDesaAntiKorupsiForm = z.object({
|
||||||
|
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const kategoriDesaAntiKorupsiDefaultForm = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const kategoriDesaAntiKorupsi = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...kategoriDesaAntiKorupsiDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = kategoriDesaAntiKorupsiForm.safeParse(
|
||||||
|
kategoriDesaAntiKorupsi.create.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
kategoriDesaAntiKorupsi.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.landingpage.kategoridak["create"].post(
|
||||||
|
kategoriDesaAntiKorupsi.create.form
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
kategoriDesaAntiKorupsi.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 {
|
||||||
|
kategoriDesaAntiKorupsi.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}> | null,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.landingpage.kategoridak["find-many"].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
kategoriDesaAntiKorupsi.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/landingpage/kategoridak/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
kategoriDesaAntiKorupsi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
kategoriDesaAntiKorupsi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
kategoriDesaAntiKorupsi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
kategoriDesaAntiKorupsi.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/landingpage/kategoridak/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Kategori desa anti korupsi berhasil dihapus");
|
||||||
|
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi");
|
||||||
|
} finally {
|
||||||
|
kategoriDesaAntiKorupsi.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
id: "",
|
||||||
|
form: { ...kategoriDesaAntiKorupsiDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/landingpage/kategoridak/${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 = {
|
||||||
|
name: data.name,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kategori desa anti korupsi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
const cek = kategoriDesaAntiKorupsiForm.safeParse(
|
||||||
|
kategoriDesaAntiKorupsi.edit.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
kategoriDesaAntiKorupsi.edit.loading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/landingpage/kategoridak/${kategoriDesaAntiKorupsi.edit.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: kategoriDesaAntiKorupsi.edit.form.name,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 desa anti korupsi (${response.status})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message ||
|
||||||
|
"Berhasil memperbarui kategori desa anti korupsi"
|
||||||
|
);
|
||||||
|
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengupdate kategori desa anti korupsi"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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 kategori desa anti korupsi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Gagal mengupdate kategori desa anti korupsi"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
kategoriDesaAntiKorupsi.edit.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
kategoriDesaAntiKorupsi.edit.id = "";
|
||||||
|
kategoriDesaAntiKorupsi.edit.form = {
|
||||||
|
...kategoriDesaAntiKorupsiDefaultForm,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const korupsiState = proxy({
|
||||||
|
desaAntikorupsi,
|
||||||
|
kategoriDesaAntiKorupsi,
|
||||||
|
});
|
||||||
|
export default korupsiState;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "List Desa Anti Korupsi",
|
||||||
|
value: "listDesaAntiKorupsi",
|
||||||
|
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori Desa Anti Korupsi",
|
||||||
|
value: "kategoriDesaAntiKorupsi",
|
||||||
|
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
const tab = tabs.find(t => t.value === value)
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab.href)
|
||||||
|
}
|
||||||
|
setActiveTab(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const match = tabs.find(tab => tab.href === pathname)
|
||||||
|
if (match) {
|
||||||
|
setActiveTab(match.value)
|
||||||
|
}
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>Desa Anti Korupsi</Title>
|
||||||
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsPanel key={i} value={e.value}>
|
||||||
|
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||||
|
<></>
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutTabs;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } 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 EditKategoriDesaAntiKorupsi() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadKategorikegiatan = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await stateKategori.edit.load(id);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// pastikan id-nya masuk ke state edit
|
||||||
|
stateKategori.edit.id = id;
|
||||||
|
setFormData({
|
||||||
|
name: data.name || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kategori desa anti korupsi:", error);
|
||||||
|
toast.error("Gagal memuat data kategori desa anti korupsi");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadKategorikegiatan();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
toast.error('Nama kategori desa anti korupsi tidak boleh kosong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateKategori.edit.form = {
|
||||||
|
name: formData.name.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safety check tambahan: pastikan ID tidak kosong
|
||||||
|
if (!stateKategori.edit.id) {
|
||||||
|
stateKategori.edit.id = id; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await stateKategori.edit.update();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating kategori desa anti korupsi:", 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 Desa Anti Korupsi</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||||
|
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditKategoriDesaAntiKorupsi;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
|
||||||
|
|
||||||
|
function CreateKategoriDesaAntiKorupsi() {
|
||||||
|
const router = useRouter();
|
||||||
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateKategori.findMany.load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
stateKategori.create.form = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await stateKategori.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Desa Anti Korupsi</Title>
|
||||||
|
<TextInput
|
||||||
|
value={stateKategori.create.form.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
stateKategori.create.form.name = val.target.value;
|
||||||
|
}}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||||
|
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateKategoriDesaAntiKorupsi;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||||
|
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 korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||||
|
|
||||||
|
|
||||||
|
function KategoriDesaAntiKorupsi() {
|
||||||
|
const [search, setSearch] = useState("")
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Kategori Desa Anti Korupsi'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListKategoriKegiatan search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListKategoriKegiatan({ search }: { search: string }) {
|
||||||
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
stateKategori.delete.byId(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateKategori.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filteredData = (stateKategori.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stateKategori.findMany.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<JudulList
|
||||||
|
title='List Kategori Kegiatan'
|
||||||
|
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
||||||
|
/>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama Kategori</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Delete</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="red" onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconX size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori kegiatan ini?'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KategoriDesaAntiKorupsi
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import LayoutTabs from './_lib/layouTabs';
|
||||||
|
|
||||||
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<LayoutTabs>
|
||||||
|
{children}
|
||||||
|
</LayoutTabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
|
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
|
|
||||||
|
interface FormDesaAntiKorupsi {
|
||||||
|
name: string;
|
||||||
|
deskripsi: string;
|
||||||
|
kategoriId: string;
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditDesaAntiKorupsi() {
|
||||||
|
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi)
|
||||||
|
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const params = useParams()
|
||||||
|
const router = useRouter()
|
||||||
|
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
|
||||||
|
name: '',
|
||||||
|
deskripsi: '',
|
||||||
|
kategoriId: '',
|
||||||
|
fileId: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDesaAntiKorupsi = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await desaAntiKorupsiState.edit.load(id);
|
||||||
|
if (data) {
|
||||||
|
// ⬇️ FIX PENTING: tambahkan ini
|
||||||
|
desaAntiKorupsiState.edit.id = id;
|
||||||
|
|
||||||
|
desaAntiKorupsiState.edit.form = {
|
||||||
|
name: data.name,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
kategoriId: data.kategoriId,
|
||||||
|
fileId: data.fileId,
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
name: data.name,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
kategoriId: data.kategoriId,
|
||||||
|
fileId: data.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.file?.link) {
|
||||||
|
setPreviewFile(data.file.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading program penghijauan:", error);
|
||||||
|
toast.error("Gagal memuat data program penghijauan");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDesaAntiKorupsi();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update global state with form data
|
||||||
|
desaAntiKorupsiState.edit.form = {
|
||||||
|
...desaAntiKorupsiState.edit.form,
|
||||||
|
name: formData.name,
|
||||||
|
deskripsi: formData.deskripsi,
|
||||||
|
kategoriId: formData.kategoriId || '',
|
||||||
|
fileId: formData.fileId // Keep existing imageId if not changed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Jika ada file baru, upload
|
||||||
|
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
|
||||||
|
desaAntiKorupsiState.edit.form.fileId = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await desaAntiKorupsiState.edit.update();
|
||||||
|
toast.success("desa anti korupsi berhasil diperbarui!");
|
||||||
|
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating desa anti korupsi:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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"}>Edit List Desa Anti Korupsi</Text>
|
||||||
|
{desaAntiKorupsiState.findUnique.data ? (
|
||||||
|
<Paper key={desaAntiKorupsiState.findUnique.data.id}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<TextInput
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
name: val.target.value
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||||
|
placeholder='Masukkan judul'
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
deskripsi: val
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
value={formData.kategoriId}
|
||||||
|
onChange={(val) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
kategoriId: val ?? ""
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
|
placeholder="Pilih kategori"
|
||||||
|
data={
|
||||||
|
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||||
|
value: v.id,
|
||||||
|
label: v.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{
|
||||||
|
'application/*': ['.pdf', '.doc', '.docx'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag file ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format document
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||||
|
{previewFile ? (
|
||||||
|
<iframe
|
||||||
|
src={previewFile}
|
||||||
|
width="100%"
|
||||||
|
height="500px"
|
||||||
|
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text>Tidak ada dokumen tersedia</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Group>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditDesaAntiKorupsi;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
'use client'
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
function DetailKegiatanDesa() {
|
||||||
|
const detailState = useProxy(korupsiState.desaAntikorupsi)
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const params = useParams()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
detailState.findUnique.load(params?.id as string)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
detailState.delete.byId(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailState.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={40} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<Text fz={"xl"} fw={"bold"}>Detail List Desa Anti Korupsi</Text>
|
||||||
|
{detailState.findUnique.data ? (
|
||||||
|
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||||
|
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||||
|
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
||||||
|
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||||
|
{detailState.findUnique.data?.file?.link ? (
|
||||||
|
<iframe
|
||||||
|
src={detailState.findUnique.data.file.link}
|
||||||
|
width="100%"
|
||||||
|
height="500px"
|
||||||
|
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text>Tidak ada dokumen tersedia</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Flex gap={"xs"} mt={10}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (detailState.findUnique.data) {
|
||||||
|
setSelectedId(detailState.findUnique.data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||||
|
color={"red"}
|
||||||
|
>
|
||||||
|
<IconX size={20} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (detailState.findUnique.data) {
|
||||||
|
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!detailState.findUnique.data}
|
||||||
|
color={"green"}
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text='Apakah anda yakin ingin menghapus desa anti korupsi ini?'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailKegiatanDesa;
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
|
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconFile, 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';
|
||||||
|
|
||||||
|
|
||||||
|
function CreateDesaAntiKorupsi() {
|
||||||
|
const router = useRouter();
|
||||||
|
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi)
|
||||||
|
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateKorupsi.findMany.load();
|
||||||
|
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
stateKorupsi.create.form = {
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
kategoriId: "",
|
||||||
|
fileId: "",
|
||||||
|
};
|
||||||
|
setFile(null);
|
||||||
|
setPreviewFile(null);
|
||||||
|
};
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn("Pilih file pdf 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
stateKorupsi.create.form.fileId = uploaded.id;
|
||||||
|
|
||||||
|
await stateKorupsi.create.create();
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
|
||||||
|
}
|
||||||
|
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 Kegiatan Desa</Title>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{
|
||||||
|
'application/*': ['.pdf', '.doc', '.docx'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag file ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format document
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||||
|
{previewFile ? (
|
||||||
|
<iframe
|
||||||
|
src={previewFile}
|
||||||
|
width="100%"
|
||||||
|
height="500px"
|
||||||
|
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text>Tidak ada dokumen tersedia</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<TextInput
|
||||||
|
value={stateKorupsi.create.form.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
stateKorupsi.create.form.name = val.target.value;
|
||||||
|
}}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||||
|
placeholder='Masukkan judul'
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={stateKorupsi.create.form.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
stateKorupsi.create.form.deskripsi = val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
value={stateKorupsi.create.form.kategoriId}
|
||||||
|
onChange={(val) => {
|
||||||
|
stateKorupsi.create.form.kategoriId = val ?? "";
|
||||||
|
}}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
|
placeholder="Pilih kategori"
|
||||||
|
data={
|
||||||
|
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||||
|
value: v.id,
|
||||||
|
label: v.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateDesaAntiKorupsi;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||||
|
|
||||||
|
function DesaAntiKorupsi() {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='List Desa Anti Korupsi'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListDesaAntiKorupsi search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListDesaAntiKorupsi({ search }: { search: string }) {
|
||||||
|
const listState = useProxy(korupsiState.desaAntikorupsi)
|
||||||
|
const router = useRouter();
|
||||||
|
useEffect(() => {
|
||||||
|
listState.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filteredData = (listState.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().includes(keyword) ||
|
||||||
|
item.deskripsi.toLowerCase().includes(keyword) ||
|
||||||
|
item.kategori?.name?.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!listState.findMany.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Desa Anti Korupsi'
|
||||||
|
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama Desa Anti Korupsi</TableTh>
|
||||||
|
<TableTh>Deskripsi Desa Anti Korupsi</TableTh>
|
||||||
|
<TableTh>Kategori Desa Anti Korupsi</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.kategori?.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}>
|
||||||
|
<IconDeviceImacCog size={25} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DesaAntiKorupsi;
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Desa Anti Korupsi
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -50,7 +50,7 @@ function DetailKegiatanDesa() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail kegiatanDesa Inovasi</Text>
|
<Text fz={"xl"} fw={"bold"}>Detail Kegiatan Desa Inovasi</Text>
|
||||||
{kegiatanDesaState.findUnique.data ? (
|
{kegiatanDesaState.findUnique.data ? (
|
||||||
<Paper key={kegiatanDesaState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper key={kegiatanDesaState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const navBar = [
|
|||||||
{
|
{
|
||||||
id: "Landing_Page_2",
|
id: "Landing_Page_2",
|
||||||
name: "Desa Anti Korupsi",
|
name: "Desa Anti Korupsi",
|
||||||
path: "/admin/landing-page/desa-anti-korupsi"
|
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Landing_Page_3",
|
id: "Landing_Page_3",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
AppShellHeader,
|
AppShellHeader,
|
||||||
AppShellMain,
|
AppShellMain,
|
||||||
AppShellNavbar,
|
AppShellNavbar,
|
||||||
|
Box,
|
||||||
Burger,
|
Burger,
|
||||||
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
NavLink,
|
NavLink,
|
||||||
@@ -15,16 +17,16 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
import { IconChevronLeft, IconChevronRight, IconDoorExit } from "@tabler/icons-react";
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { navBar } from "./_com/list_PageAdmin";
|
import { navBar } from "./_com/list_PageAdmin";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle }] = useDisclosure();
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
|
const router = useRouter()
|
||||||
// Normalisasi semua segmen jadi lowercase
|
// Normalisasi semua segmen jadi lowercase
|
||||||
const segments = useSelectedLayoutSegments().map(s => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map(s => _.lowerCase(s));
|
||||||
|
|
||||||
@@ -44,6 +46,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
<AppShellHeader bg={colors["white-1"]}>
|
<AppShellHeader bg={colors["white-1"]}>
|
||||||
<Group px={10} align="center">
|
<Group px={10} align="center">
|
||||||
|
<Flex align="center" gap={'xs'}>
|
||||||
|
<Image
|
||||||
|
py={5}
|
||||||
|
src={'/assets/images/darmasaba-icon.png'}
|
||||||
|
alt=""
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
/>
|
||||||
|
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}>
|
||||||
|
Dashboard Admin
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
{!desktopOpened && (
|
{!desktopOpened && (
|
||||||
<ActionIcon variant="light" onClick={toggleDesktop}>
|
<ActionIcon variant="light" onClick={toggleDesktop}>
|
||||||
<IconChevronRight />
|
<IconChevronRight />
|
||||||
@@ -55,6 +69,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
hiddenFrom="sm"
|
hiddenFrom="sm"
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
/>
|
/>
|
||||||
|
<Box>
|
||||||
|
<ActionIcon onClick={() => {
|
||||||
|
router.push("/darmasaba")
|
||||||
|
}} color={colors["blue-button"]} radius={'xl'}>
|
||||||
|
<IconDoorExit size={24} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
w={50}
|
w={50}
|
||||||
h={50}
|
h={50}
|
||||||
@@ -62,17 +83,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
component={Link}
|
component={Link}
|
||||||
href="/admin"
|
href="/admin"
|
||||||
>
|
>
|
||||||
<Image
|
|
||||||
py={5}
|
|
||||||
src={'/assets/images/darmasaba-icon.png'}
|
|
||||||
alt=""
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}>
|
|
||||||
Dashboard Admin
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -14,27 +14,15 @@ const fileStorageCreate = async (context: Context) => {
|
|||||||
const file = body.file;
|
const file = body.file;
|
||||||
const name = body.name;
|
const name = body.name;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) return { status: 400, body: "No file uploaded" };
|
||||||
return {
|
if (!name) return { status: 400, body: "No name provided" };
|
||||||
status: 400,
|
if (!UPLOAD_DIR) return { status: 500, body: "UPLOAD_DIR is not defined" };
|
||||||
body: "No file uploaded",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name) {
|
// Tentukan kategori berdasarkan mimeType
|
||||||
return {
|
const isImage = file.type.startsWith("image/");
|
||||||
status: 400,
|
const category = isImage ? "image" : "document";
|
||||||
body: "No name provided",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!UPLOAD_DIR) {
|
const pathName = category === "image" ? "images" : "documents";
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: "UPLOAD_DIR is not defined",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const pathName = "allFile";
|
|
||||||
const rootPath = path.join(UPLOAD_DIR, pathName);
|
const rootPath = path.join(UPLOAD_DIR, pathName);
|
||||||
await fs.mkdir(rootPath, { recursive: true });
|
await fs.mkdir(rootPath, { recursive: true });
|
||||||
|
|
||||||
@@ -44,9 +32,10 @@ const fileStorageCreate = async (context: Context) => {
|
|||||||
const data = await prisma.fileStorage.create({
|
const data = await prisma.fileStorage.create({
|
||||||
data: {
|
data: {
|
||||||
name: newName,
|
name: newName,
|
||||||
realName: file.name, // Add the original file name as realName
|
realName: file.name,
|
||||||
path: rootPath,
|
path: rootPath,
|
||||||
mimeType: file.type,
|
mimeType: file.type,
|
||||||
|
category,
|
||||||
link: `/api/fileStorage/findUnique/${newName}`,
|
link: `/api/fileStorage/findUnique/${newName}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -56,9 +45,7 @@ const fileStorageCreate = async (context: Context) => {
|
|||||||
Buffer.from(await file.arrayBuffer())
|
Buffer.from(await file.arrayBuffer())
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { data };
|
||||||
data,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default fileStorageCreate;
|
export default fileStorageCreate;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export const fileStorageFindMany = async () => {
|
export const fileStorageFindMany = async (context: Context) => {
|
||||||
const data = await prisma.fileStorage.findMany();
|
const category = context.query?.category as string | undefined;
|
||||||
return data;
|
|
||||||
|
const data = await prisma.fileStorage.findMany({
|
||||||
|
where: category ? { category } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreate = {
|
||||||
|
name: string;
|
||||||
|
deskripsi: string;
|
||||||
|
fileId: string;
|
||||||
|
kategoriId: string; // minimal satu kategori
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function desaAntiKorupsiCreate(context: Context) {
|
||||||
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
|
if (!body.kategoriId) {
|
||||||
|
throw new Error("kategoriId wajib diisi");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create langsung data AdministrasiOnline
|
||||||
|
const result = await prisma.desaAntiKorupsi.create({
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
deskripsi: body.deskripsi,
|
||||||
|
fileId: body.fileId,
|
||||||
|
kategoriId: body.kategoriId, // relasi ke JenisLayanan
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
kategori: true, // Include data relasi
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil membuat desa anti korupsi",
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating desa anti korupsi:", error);
|
||||||
|
throw new Error(
|
||||||
|
"Gagal membuat desa anti korupsi: " + (error as Error).message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function desaAntiKorupsiDelete(context: Context) {
|
||||||
|
const { params } = context;
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("ID tidak ditemukan dalam parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await prisma.desaAntiKorupsi.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil menghapus desa anti korupsi",
|
||||||
|
data: deleted,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// /api/berita/findManyPaginated.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
async function desaAntiKorupsiFindMany(context: Context) {
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.desaAntiKorupsi.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: {
|
||||||
|
kategori: true,
|
||||||
|
file: true,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
|
||||||
|
}),
|
||||||
|
prisma.desaAntiKorupsi.count({
|
||||||
|
where: { isActive: true }
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch desa anti korupsi with pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Find many paginated error:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed fetch desa anti korupsi with pagination",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default desaAntiKorupsiFindMany;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function desaAntiKorupsiFindUnique(context: Context) {
|
||||||
|
const { params } = context;
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("ID tidak ditemukan dalam parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await prisma.desaAntiKorupsi.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
kategori: true,
|
||||||
|
file: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new Error("Desa anti korupsi tidak ditemukan");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Data desa anti korupsi ditemukan",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import desaAntiKorupsiDelete from "./del";
|
||||||
|
import desaAntiKorupsiFindMany from "./findMany";
|
||||||
|
import desaAntiKorupsiFindUnique from "./findUnique";
|
||||||
|
import desaAntiKorupsiUpdate from "./updt";
|
||||||
|
import desaAntiKorupsiCreate from "./create";
|
||||||
|
|
||||||
|
const DesaAntiKorupsi = new Elysia({
|
||||||
|
prefix: "/desaantikorupsi",
|
||||||
|
tags: ["Landing Page/Desa Anti Korupsi"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Find all
|
||||||
|
.get("/find-many", desaAntiKorupsiFindMany)
|
||||||
|
|
||||||
|
// ✅ Find by ID
|
||||||
|
.get("/:id", desaAntiKorupsiFindUnique)
|
||||||
|
|
||||||
|
// ✅ Create
|
||||||
|
.post("/create", desaAntiKorupsiCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
fileId: t.String(),
|
||||||
|
kategoriId: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Update
|
||||||
|
.put("/:id", desaAntiKorupsiUpdate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
fileId: t.String(),
|
||||||
|
kategoriId: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
// ✅ Delete
|
||||||
|
.delete("/del/:id", desaAntiKorupsiDelete);
|
||||||
|
|
||||||
|
export default DesaAntiKorupsi;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function kategoriDesaAntiKorupsiCreate(context: Context) {
|
||||||
|
const body = context.body as {name: string};
|
||||||
|
|
||||||
|
if (!body.name) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Nama is required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategoriDesaAntiKorupsi = await prisma.kategoriDesaAntiKorupsi.create({
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success create kategori desa anti korupsi",
|
||||||
|
data: kategoriDesaAntiKorupsi
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function kategoriDesaAntiKorupsiDelete(context: Context) {
|
||||||
|
const { params } = context;
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("ID tidak ditemukan dalam parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await prisma.kategoriDesaAntiKorupsi.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil menghapus kategori desa anti korupsi",
|
||||||
|
data: deleted,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function kategoriDesaAntiKorupsiFindMany() {
|
||||||
|
const data = await prisma.kategoriDesaAntiKorupsi.findMany();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data.map((item: any) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function kategoriDesaAntiKorupsiFindUnique(context: Context) {
|
||||||
|
const url = new URL(context.request.url);
|
||||||
|
const pathSegments = url.pathname.split('/');
|
||||||
|
const id = pathSegments[pathSegments.length - 1];
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID is required",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof id !== 'string') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID is required",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await prisma.kategoriDesaAntiKorupsi.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Kategori desa anti korupsi tidak ditemukan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success find kategori desa anti korupsi",
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find by ID error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil kategori desa anti korupsi: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import kategoriDesaAntiKorupsiCreate from "./create";
|
||||||
|
import kategoriDesaAntiKorupsiDelete from "./del";
|
||||||
|
import kategoriDesaAntiKorupsiFindMany from "./findMany";
|
||||||
|
import kategoriDesaAntiKorupsiFindUnique from "./findUnique";
|
||||||
|
import kategoriDesaAntiKorupsiUpdate from "./updt";
|
||||||
|
|
||||||
|
const KategoriDesaAntiKorupsi = new Elysia({
|
||||||
|
prefix: "/kategoridak",
|
||||||
|
tags: ["Landing Page/Desa Anti Korupsi"],
|
||||||
|
})
|
||||||
|
.get("/find-many", kategoriDesaAntiKorupsiFindMany)
|
||||||
|
.get("/:id", async (context) => {
|
||||||
|
const response = await kategoriDesaAntiKorupsiFindUnique(context);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.delete("/del/:id", kategoriDesaAntiKorupsiDelete)
|
||||||
|
.post("/create", kategoriDesaAntiKorupsiCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.put("/:id", kategoriDesaAntiKorupsiUpdate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default KategoriDesaAntiKorupsi;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function kategoriDesaAntiKorupsiUpdate(context: Context) {
|
||||||
|
const body = context.body as { name: string };
|
||||||
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
|
// Validasi ID dan nama
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID is required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.name) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Name is required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kategoriDesaAntiKorupsi = await prisma.kategoriDesaAntiKorupsi.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success update kategori desa anti korupsi",
|
||||||
|
data: kategoriDesaAntiKorupsi,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal update kategori desa anti korupsi",
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormUpdateDesaAntiKorupsi = {
|
||||||
|
name?: string;
|
||||||
|
deskripsi?: string;
|
||||||
|
fileId?: string;
|
||||||
|
kategoriId?: string; // minimal satu kategori
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function desaAntiKorupsiUpdate(context: Context) {
|
||||||
|
const body = context.body as FormUpdateDesaAntiKorupsi;
|
||||||
|
|
||||||
|
const id = context.params.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID desa anti korupsi wajib diisi",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await prisma.desaAntiKorupsi.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
deskripsi: body.deskripsi,
|
||||||
|
fileId: body.fileId,
|
||||||
|
kategoriId: body.kategoriId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
file: true,
|
||||||
|
kategori: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Desa anti korupsi berhasil diperbarui",
|
||||||
|
data: updated,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Error update desa anti korupsi:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal memperbarui data desa anti korupsi",
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import Elysia from "elysia";
|
|||||||
import MediaSosial from "./profile/media-sosial";
|
import MediaSosial from "./profile/media-sosial";
|
||||||
import ProgramInovasi from "./profile/program-inovasi";
|
import ProgramInovasi from "./profile/program-inovasi";
|
||||||
import PejabatDesa from "./profile/pejabat-desa";
|
import PejabatDesa from "./profile/pejabat-desa";
|
||||||
|
import KategoriDesaAntiKorupsi from "./desa-anti-korupsi/kategori-dak";
|
||||||
|
import DesaAntiKorupsi from "./desa-anti-korupsi";
|
||||||
|
|
||||||
const LandingPage = new Elysia({
|
const LandingPage = new Elysia({
|
||||||
prefix: "/api/landingpage",
|
prefix: "/api/landingpage",
|
||||||
@@ -11,5 +13,7 @@ const LandingPage = new Elysia({
|
|||||||
.use(MediaSosial)
|
.use(MediaSosial)
|
||||||
.use(ProgramInovasi)
|
.use(ProgramInovasi)
|
||||||
.use(PejabatDesa)
|
.use(PejabatDesa)
|
||||||
|
.use(KategoriDesaAntiKorupsi)
|
||||||
|
.use(DesaAntiKorupsi)
|
||||||
|
|
||||||
export default LandingPage
|
export default LandingPage
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function Navbar() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavbarMainMenu listNavbar={navbarListMenu} />
|
<NavbarMainMenu listNavbar={navbarListMenu} />
|
||||||
|
|
||||||
<Stack hiddenFrom="sm" bg={colors.grey[2]}>
|
<Stack hiddenFrom="sm" bg={colors.grey[2]}>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<ActionIcon variant="transparent" onClick={() => {
|
<ActionIcon variant="transparent" onClick={() => {
|
||||||
@@ -50,6 +51,7 @@ export function Navbar() {
|
|||||||
<NavbarMobile listNavbar={navbarListMenu} />
|
<NavbarMobile listNavbar={navbarListMenu} />
|
||||||
</motion.div>}
|
</motion.div>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
{(item || isSearch) && <Box className="glass" />}
|
{(item || isSearch) && <Box className="glass" />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,18 +4,20 @@ import colors from "@/con/colors"
|
|||||||
import stateNav from "@/state/state-nav"
|
import stateNav from "@/state/state-nav"
|
||||||
import { ActionIcon, Button, Container, Flex, Image, Stack } from "@mantine/core"
|
import { ActionIcon, Button, Container, Flex, Image, Stack } from "@mantine/core"
|
||||||
import { useHover } from "@mantine/hooks"
|
import { useHover } from "@mantine/hooks"
|
||||||
|
import { IconSearch, IconUser } from "@tabler/icons-react"
|
||||||
import { useTransitionRouter } from 'next-view-transitions'
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
import { useSnapshot } from "valtio"
|
import { useSnapshot } from "valtio"
|
||||||
import { MenuItem } from "../../../../types/menu-item"
|
import { MenuItem } from "../../../../types/menu-item"
|
||||||
import { NavbarSearch } from "./NavBarSearch"
|
import { NavbarSearch } from "./NavBarSearch"
|
||||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||||
import { IconSearch } from "@tabler/icons-react"
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
export function NavbarMainMenu({ listNavbar }: {
|
export function NavbarMainMenu({ listNavbar }: {
|
||||||
listNavbar: MenuItem[]
|
listNavbar: MenuItem[]
|
||||||
}) {
|
}) {
|
||||||
const { item, isSearch } = useSnapshot(stateNav)
|
const { item, isSearch } = useSnapshot(stateNav)
|
||||||
const router = useTransitionRouter()
|
const router = useTransitionRouter()
|
||||||
|
const next = useRouter()
|
||||||
return <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
return <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
||||||
<Container pos={"relative"} w={{
|
<Container pos={"relative"} w={{
|
||||||
base: '100%',
|
base: '100%',
|
||||||
@@ -44,6 +46,11 @@ export function NavbarMainMenu({ listNavbar }: {
|
|||||||
{/* TODO: add icon search */}
|
{/* TODO: add icon search */}
|
||||||
<IconSearch size={"1.5rem"} />
|
<IconSearch size={"1.5rem"} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
<ActionIcon onClick={() => {
|
||||||
|
next.push("/admin/landing-page/profile/program-inovasi")
|
||||||
|
}} color={colors["blue-button"]} radius={'xl'}>
|
||||||
|
<IconUser size={24} />
|
||||||
|
</ActionIcon>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
{item && <NavbarSubMenu item={item as MenuItem[]} />}
|
{item && <NavbarSubMenu item={item as MenuItem[]} />}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function Page() {
|
|||||||
|
|
||||||
const loadListFile = async () => {
|
const loadListFile = async () => {
|
||||||
const { data } = await ApiFetch.api.fileStorage.findMany.get()
|
const { data } = await ApiFetch.api.fileStorage.findMany.get()
|
||||||
setListFile(data?.map((item) => item.link) || [])
|
setListFile(data?.data?.map((item) => item.link) || [])
|
||||||
}
|
}
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
loadListFile()
|
loadListFile()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "PPID",
|
name: "PPID",
|
||||||
href: "/darmasaba/ppid",
|
href: "/darmasaba/ppid/profile-ppid",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "1.1",
|
id: "1.1",
|
||||||
@@ -51,7 +51,7 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "Desa",
|
name: "Desa",
|
||||||
href: "/darmasaba/desa",
|
href: "/darmasaba/desa/profile",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "2.1",
|
id: "2.1",
|
||||||
@@ -94,7 +94,7 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
name: "Kesehatan",
|
name: "Kesehatan",
|
||||||
href: "/darmasaba/kesehatan",
|
href: "/darmasaba/kesehatan/posyandu",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "3.1",
|
id: "3.1",
|
||||||
@@ -136,7 +136,7 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "4",
|
id: "4",
|
||||||
name: "Keamanan",
|
name: "Keamanan",
|
||||||
href: "/darmasaba/keamanan",
|
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "4.1",
|
id: "4.1",
|
||||||
@@ -173,7 +173,7 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "5",
|
id: "5",
|
||||||
name: "Ekonomi",
|
name: "Ekonomi",
|
||||||
href: "/darmasaba/ekonomi",
|
href: "/darmasaba/ekonomi/pasar-desa",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "5.1",
|
id: "5.1",
|
||||||
@@ -229,7 +229,7 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "6",
|
id: "6",
|
||||||
name: "Inovasi",
|
name: "Inovasi",
|
||||||
href: "/darmasaba/inovasi",
|
href: "/darmasaba/inovasi/desa-digital-smart-village",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "6.1",
|
id: "6.1",
|
||||||
@@ -266,7 +266,7 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "7",
|
id: "7",
|
||||||
name: "Lingkungan",
|
name: "Lingkungan",
|
||||||
href: "/darmasaba/lingkungan",
|
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "7.1",
|
id: "7.1",
|
||||||
@@ -302,7 +302,7 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "8",
|
id: "8",
|
||||||
name: "Pendidikan",
|
name: "Pendidikan",
|
||||||
href: "/darmasaba/pendidikan",
|
href: "/darmasaba/pendidikan/info-sekolah-paud",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "8.1",
|
id: "8.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user