Compare commits

...

10 Commits

368 changed files with 12131 additions and 6790 deletions

View File

@@ -81,8 +81,6 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
InfoTekno InfoTekno[]
@@ -101,6 +99,8 @@ model FileStorage {
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -672,17 +672,18 @@ model GalleryVideo {
// ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AjukanPermohonan AjukanPermohonan[]
}
model PelayananTelunjukSaktiDesa {
@@ -717,6 +718,20 @@ model PelayananPendudukNonPermanen {
isActive Boolean @default(true)
}
model AjukanPermohonan {
id String @id @default(cuid())
nama String
nik String
alamat String
nomorKk String
kategori PelayananSuratKeterangan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENGHARGAAN ========================================= //
model Penghargaan {
id String @id @default(cuid())
@@ -835,8 +850,8 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -972,8 +987,10 @@ model ArtikelKesehatan {
id String @id @default(cuid())
title String
content String
introduction Introduction @relation(fields: [introductionId], references: [id])
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
introductionId String
introduction Introduction @relation(fields: [introductionId], references: [id])
symptom Symptom @relation(fields: [symptomId], references: [id])
symptomId String
prevention Prevention @relation(fields: [preventionId], references: [id])
@@ -1218,8 +1235,7 @@ model LayananPolsek {
model KontakDaruratKeamanan {
id String @id @default(uuid())
nama String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
icon String
kategori KontakItem @relation(fields: [kategoriId], references: [id])
kategoriId String
kontakItems KontakDaruratToItem[]
@@ -1233,8 +1249,7 @@ model KontakItem {
id String @id @default(uuid())
nama String
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1254,48 +1269,15 @@ model KontakDaruratToItem {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas {
id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id])
programKeamananId String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id])
tipsKeamananId String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id])
videoKeamananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramKeamanan {
id String @id @default(cuid())
nama String // contoh: "Ronda Malam"
deskripsi String? // jika mau tambahkan info detail
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model TipsKeamanan {
id String @id @default(cuid())
judul String
konten String
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model VideoKeamanan {
id String @id @default(cuid())
judul String
deskripsi String?
videoUrl String // link youtube atau embed url
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
id String @id @default(cuid())
judul String
deskripsi String
deskripsiSingkat String
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAPORAN PUBLIK ========================================= //
@@ -1304,11 +1286,13 @@ model LaporanPublik {
judul String
lokasi String
tanggalWaktu DateTime
status StatusLaporan
status StatusLaporan @default(Proses)
penanganan PenangananLaporanPublik[]
kronologi String? // Optional, bisa diisi detail kronologi
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PenangananLaporanPublik {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -23,6 +23,10 @@ import {
IconSchool,
IconShoppingCart,
IconHospital,
IconAmbulance,
IconFiretruck,
IconBuilding,
IconAlertTriangle,
} from '@tabler/icons-react'
export type IconKey =
@@ -46,6 +50,13 @@ export type IconKey =
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
| 'polisi'
| 'ambulans'
| 'pemadam'
| 'rumahSakit'
| 'bangunan'
| 'darurat'
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -68,6 +79,12 @@ const iconMap: Record<IconKey, React.FC<any>> = {
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
polisi: IconShieldFilled,
ambulans: IconAmbulance,
pemadam: IconFiretruck,
rumahSakit: IconHospital,
bangunan: IconBuilding,
darurat: IconAlertTriangle
}
type Props = {

View File

@@ -3,11 +3,15 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconAlertTriangle,
IconAmbulance,
IconBuilding,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconFiretruck,
IconHome,
IconHomeEco,
IconHospital,
@@ -47,6 +51,12 @@ const iconMap = {
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: { label: 'Bangunan', icon: IconBuilding },
darurat: { label: 'Darurat', icon: IconAlertTriangle },
};

View File

@@ -2,11 +2,13 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconAmbulance,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconFiretruck,
IconHome,
IconHomeEco,
IconHospital,
@@ -22,6 +24,8 @@ import {
IconTrendingUp,
IconTrophy,
IconTruckFilled,
IconBuilding,
IconAlertTriangle
} from '@tabler/icons-react';
const iconMap = {
@@ -45,6 +49,12 @@ const iconMap = {
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
polisi: {label: 'Polisi', icon: IconShieldFilled},
ambulans: {label: 'Ambulans', icon: IconAmbulance},
pemadam: {label: 'Pemadam', icon: IconFiretruck},
rumahSakit: {label: 'Rumah Sakit', icon: IconHospital},
bangunan: {label: 'Bangunan', icon: IconBuilding},
darurat: {label: 'Darurat', icon: IconAlertTriangle},
};
type IconKey = keyof typeof iconMap;

View File

@@ -71,6 +71,22 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "",
};
const templateAjukanForm = z.object({
nama: z.string().min(1).max(5000),
nik: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
nomorKk: z.string().min(1).max(5000),
kategoriId: z.string().min(1).max(5000),
});
const defaultAjukanForm = {
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
};
const suratKeterangan = proxy({
create: {
form: { ...suratKeteranganForm },
@@ -146,6 +162,30 @@ const suratKeterangan = proxy({
}
},
},
findManyAll: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
omit: { isActive: true };
}>[] | null,
loading: false,
load: async () => {
suratKeterangan.findManyAll.loading = true;
try {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan["findManyAll"].get();
if (res.status === 200 && res.data?.success) {
suratKeterangan.findManyAll.data = res.data.data || [];
} else {
suratKeterangan.findManyAll.data = [];
console.error("Failed to load surat keterangan all:", res.data?.message);
}
} catch (error) {
console.error("Error loading surat keterangan all:", error);
suratKeterangan.findManyAll.data = [];
} finally {
suratKeterangan.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: {
@@ -769,11 +809,250 @@ const pelayananPendudukNonPermanen = proxy({
},
});
const ajukanPermohonan = proxy({
create: {
form: { ...defaultAjukanForm },
loading: false,
async create() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.create.loading = true;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"create"
].post(ajukanPermohonan.create.form);
if (res.status === 200) {
ajukanPermohonan.findMany.load();
return toast.success("Ajukan permohonan berhasil disimpan!");
}
return toast.error("Gagal menyimpan ajukan permohonan");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanPermohonan.create.loading = false;
}
},
resetForm() {
ajukanPermohonan.create.form = { ...defaultAjukanForm };
},
},
findMany: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
};
}>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
ajukanPermohonan.findMany.loading = true; // Use the full path to access the property
ajukanPermohonan.findMany.page = page;
ajukanPermohonan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"findMany"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
ajukanPermohonan.findMany.data = res.data.data || [];
ajukanPermohonan.findMany.total = res.data.total || 0;
ajukanPermohonan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load ajukan permohonan:", res.data?.message);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading ajukan permohonan:", error);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
} finally {
ajukanPermohonan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/ajukanpermohonan/${id}`
);
if (res.ok) {
const data = await res.json();
ajukanPermohonan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch ajukan permohonan:", res.statusText);
ajukanPermohonan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
ajukanPermohonan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanPermohonan.delete.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan permohonan berhasil dihapus");
await ajukanPermohonan.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus ajukan permohonan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus ajukan permohonan");
} finally {
ajukanPermohonan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultAjukanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/ajukanpermohonan/${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,
nik: data.nik,
alamat: data.alamat,
nomorKk: data.nomorKk,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.edit.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nik: this.form.nik,
alamat: this.form.alamat,
nomorKk: this.form.nomorKk,
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(result.message || "Ajukan permohonan berhasil diupdate");
await ajukanPermohonan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate ajukan permohonan"
);
}
} catch (error) {
console.error("Error updating ajukan permohonan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update ajukan permohonan"
);
return false;
} finally {
ajukanPermohonan.edit.loading = false;
}
},
},
});
const stateLayananDesa = proxy({
suratKeterangan,
pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanen,
ajukanPermohonan,
});
export default stateLayananDesa;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -61,10 +62,37 @@ const ajukanIdeInovatifState = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get();
if (res.status === 200) {
ajukanIdeInovatifState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
ajukanIdeInovatifState.findMany.loading = true; // ✅ Akses langsung via nama path
ajukanIdeInovatifState.findMany.page = page;
ajukanIdeInovatifState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.ajukanideinovatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
ajukanIdeInovatifState.findMany.data = res.data.data ?? [];
ajukanIdeInovatifState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch ajukan ide inovatif paginated:", err);
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
} finally {
ajukanIdeInovatifState.findMany.loading = false;
}
},
},
@@ -97,16 +125,21 @@ const ajukanIdeInovatifState = proxy({
try {
ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/inovasi/ajukanideinovatif/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus");
toast.success(
result.message || "Ajukan Ide Inovatif berhasil dihapus"
);
await ajukanIdeInovatifState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -54,19 +55,20 @@ const administrasiOnline = proxy({
},
findMany: {
data: null as Array<
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
search: "",
async load(page = 1, limit = 10, search = "") {
administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page;
administrasiOnline.findMany.search = search;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
@@ -75,6 +77,7 @@ const administrasiOnline = proxy({
query: {
page,
limit,
search,
},
});
@@ -91,10 +94,10 @@ const administrasiOnline = proxy({
},
findUnique: {
data: null as Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}> | null,
include: {
jenisLayanan: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
@@ -199,13 +202,37 @@ const jenisLayanan = proxy({
nama: string;
deskripsi: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get();
if (res.status === 200) {
jenisLayanan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jenisLayanan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisLayanan.findMany.page = page;
jenisLayanan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisLayanan.findMany.data = res.data.data ?? [];
jenisLayanan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis layanan paginated:", err);
jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1;
} finally {
jenisLayanan.findMany.loading = false;
}
},
},
@@ -403,7 +430,9 @@ const templatePengaduanMasyarakatForm = z.object({
nik: z.string().min(1, "NIK minimal 1 karakter"),
judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
deskripsiPengaduan: z.string().min(1, "Deskripsi pengaduan minimal 1 karakter"),
deskripsiPengaduan: z
.string()
.min(1, "Deskripsi pengaduan minimal 1 karakter"),
jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
imageId: z.string().min(1, "Image minimal 1 karakter"),
});
@@ -455,37 +484,42 @@ const pengaduanMasyarakat = proxy({
},
findMany: {
data: null as Array<
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
pengaduanMasyarakat.findMany.loading = true;
search: "",
load: async (page = 1, limit = 10, search = "") => {
pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
pengaduanMasyarakat.findMany.page = page;
pengaduanMasyarakat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many"
].get({
query: {
page,
limit,
},
});
].get({ query });
if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err);
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} finally {
pengaduanMasyarakat.findMany.loading = false;
}
@@ -493,11 +527,11 @@ const pengaduanMasyarakat = proxy({
},
findUnique: {
data: null as Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
@@ -507,7 +541,10 @@ const pengaduanMasyarakat = proxy({
const data = await res.json();
pengaduanMasyarakat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengaduan masyarakat:", res.statusText);
console.error(
"Failed to fetch pengaduan masyarakat:",
res.statusText
);
pengaduanMasyarakat.findUnique.data = null;
}
} catch (error) {
@@ -542,7 +579,9 @@ const pengaduanMasyarakat = proxy({
);
await pengaduanMasyarakat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengaduan masyarakat");
toast.error(
result?.message || "Gagal menghapus pengaduan masyarakat"
);
}
} catch (error) {
console.error("Gagal delete:", error);
@@ -567,7 +606,9 @@ const jenisPengaduan = proxy({
form: { ...defaultJenisPengaduanForm },
loading: false,
async create() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.create.form);
const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -598,13 +639,37 @@ const jenisPengaduan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get();
if (res.status === 200) {
jenisPengaduan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisPengaduan.findMany.page = page;
jenisPengaduan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisPengaduan.findMany.data = res.data.data ?? [];
jenisPengaduan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis pengaduan paginated:", err);
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
} finally {
jenisPengaduan.findMany.loading = false;
}
},
},
@@ -693,7 +758,7 @@ const jenisPengaduan = proxy({
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama
nama: data.nama,
};
return data;
} else {
@@ -709,7 +774,9 @@ const jenisPengaduan = proxy({
},
async update() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.edit.form);
const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -759,7 +826,9 @@ const jenisPengaduan = proxy({
await jenisPengaduan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis pengaduan");
throw new Error(
result.message || "Gagal mengupdate jenis pengaduan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
@@ -792,7 +861,6 @@ const jenisPengaduan = proxy({
},
});
const layananonlineDesa = proxy({
administrasiOnline,
jenisLayanan,

View File

@@ -54,34 +54,32 @@ const programKreatifState = proxy({
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
programKreatifState.findMany.loading = true; // Use the full path to access the property
search: "",
load: async (page = 1, limit = 10, search = "") => {
programKreatifState.findMany.loading = true; // ✅ Akses langsung via nama path
programKreatifState.findMany.page = page;
programKreatifState.findMany.search = search;
try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({
query: { page, limit },
});
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.inovasi.programkreatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || [];
programKreatifState.findMany.total = res.data.total || 0;
programKreatifState.findMany.totalPages = res.data.totalPages || 1;
programKreatifState.findMany.data = res.data.data ?? [];
programKreatifState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
} catch (err) {
console.error("Gagal fetch program kreatif paginated:", err);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
} finally {
programKreatifState.findMany.loading = false;

View File

@@ -7,13 +7,13 @@ import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(),
icon: z.string().nonempty(),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
});
const defaultForm = {
nama: "",
imageId: "",
icon: "",
kategoriId: [] as string[],
};
@@ -54,7 +54,6 @@ const kontakDaruratKeamananState = proxy({
Prisma.KontakDaruratKeamananGetPayload<{
include: {
kategori: true;
image: true;
kontakItems: {
include: {
kontakItem: true;
@@ -102,14 +101,9 @@ const kontakDaruratKeamananState = proxy({
include: {
kontakItems: {
include: {
kontakItem: {
include: {
image: true;
}
};
kontakItem: true;
};
};
image: true;
kategori: true;
};
}> | null,
@@ -192,8 +186,9 @@ const kontakDaruratKeamananState = proxy({
this.id = data.id;
this.form = {
nama: data.nama,
imageId: data.imageId || '',
kategoriId: data.kontakItems?.map((item: any) => item.kontakItemId) || []
icon: data.icon || "",
kategoriId:
data.kontakItems?.map((item: any) => item.kontakItemId) || [],
};
return data;
} else {
@@ -230,7 +225,7 @@ const kontakDaruratKeamananState = proxy({
},
body: JSON.stringify({
nama: this.form.nama,
imageId: this.form.imageId,
icon: this.form.icon,
kategoriId: this.form.kategoriId,
}),
}
@@ -271,13 +266,13 @@ const kontakDaruratKeamananState = proxy({
const templateFormItem = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
icon: z.string().nonempty(),
});
const defaultFormItem = {
nama: "",
nomorTelepon: "",
imageId: "",
icon: "",
};
const kontakDaruratItem = proxy({
@@ -285,9 +280,7 @@ const kontakDaruratItem = proxy({
form: { ...defaultFormItem },
loading: false,
async create() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.create.form
);
const cek = templateFormItem.safeParse(kontakDaruratItem.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -296,9 +289,9 @@ const kontakDaruratItem = proxy({
}
try {
kontakDaruratItem.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakitem[
"create"
].post(kontakDaruratItem.create.form);
const res = await ApiFetch.api.keamanan.kontakitem["create"].post(
kontakDaruratItem.create.form
);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
return toast.success("success create");
@@ -315,8 +308,8 @@ const kontakDaruratItem = proxy({
findMany: {
data: null as Array<
Prisma.KontakItemGetPayload<{
include: {
image: true;
omit: {
isActive: true;
};
}>
> | null,
@@ -333,14 +326,13 @@ const kontakDaruratItem = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakitem[
"find-many"
].get({ query });
const res = await ApiFetch.api.keamanan.kontakitem["find-many"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kontakDaruratItem.findMany.data = res.data.data ?? [];
kontakDaruratItem.findMany.totalPages =
res.data.totalPages ?? 1;
kontakDaruratItem.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
@@ -356,9 +348,8 @@ const kontakDaruratItem = proxy({
},
findUnique: {
data: null as Prisma.KontakItemGetPayload<{
include: {
kategori: true;
image: true;
omit: {
isActive: true;
};
}> | null,
loading: false,
@@ -384,15 +375,12 @@ const kontakDaruratItem = proxy({
if (!id) return toast.warn("ID tidak valid");
try {
kontakDaruratItem.delete.loading = true;
const response = await fetch(
`/api/keamanan/kontakitem/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/keamanan/kontakitem/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
@@ -422,15 +410,12 @@ const kontakDaruratItem = proxy({
}
try {
const response = await fetch(
`/api/keamanan/kontakitem/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/keamanan/kontakitem/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -441,7 +426,7 @@ const kontakDaruratItem = proxy({
this.form = {
nama: data.nama,
nomorTelepon: data.nomorTelepon,
imageId: data.imageId,
icon: data.icon,
};
return data;
} else {
@@ -457,9 +442,7 @@ const kontakDaruratItem = proxy({
},
async update() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.update.form
);
const cek = templateFormItem.safeParse(kontakDaruratItem.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -469,20 +452,17 @@ const kontakDaruratItem = proxy({
try {
kontakDaruratItem.update.loading = true;
const response = await fetch(
`/api/keamanan/kontakitem/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nomorTelepon: this.form.nomorTelepon,
imageId: this.form.imageId,
}),
}
);
const response = await fetch(`/api/keamanan/kontakitem/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nomorTelepon: this.form.nomorTelepon,
icon: this.form.icon,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
@@ -514,7 +494,7 @@ const kontakDaruratItem = proxy({
kontakDaruratItem.update.form = { ...defaultFormItem };
},
},
})
});
const kontakDarurat = proxy({
kontakDaruratKeamananState,

View File

@@ -11,12 +11,24 @@ const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"),
status: z.enum(["Selesai", "Proses", "Gagal"]),
penanganan: z.string(),
kronologi: z.string().optional(),
});
interface FormData {
judul: string;
lokasi: string;
tanggalWaktu: string;
kronologi: string;
}
const defaultForm: FormData = {
judul: "",
lokasi: "",
tanggalWaktu: new Date().toISOString(),
kronologi: "",
};
interface FormEditData {
judul: string;
lokasi: string;
tanggalWaktu: string;
@@ -25,15 +37,16 @@ interface FormData {
kronologi: string;
}
const defaultForm: FormData = {
const editForm: FormEditData = {
judul: "",
lokasi: "",
tanggalWaktu: new Date().toISOString(),
kronologi: "",
status: "Proses",
penanganan: "",
kronologi: "",
};
const laporanPublikState = proxy({
create: {
form: { ...defaultForm },
@@ -185,7 +198,7 @@ const laporanPublikState = proxy({
},
edit: {
id: "",
form: { ...defaultForm },
form: { ...editForm },
loading: false,
async load(id: string) {
if (!id) {
@@ -291,7 +304,7 @@ const laporanPublikState = proxy({
},
reset() {
laporanPublikState.edit.id = "";
laporanPublikState.edit.form = { ...defaultForm };
laporanPublikState.edit.form = { ...editForm };
},
}
});

View File

@@ -6,45 +6,17 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
pencegahanKriminalitas: z.object({
programKeamanan: z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
});
const defaultForm = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
judul: "",
deskripsi: "",
deskripsiSingkat: "",
linkVideo: "",
};
const pencegahanKriminalitasState = proxy({
@@ -65,7 +37,7 @@ const pencegahanKriminalitasState = proxy({
pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas);
].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
@@ -82,11 +54,7 @@ const pencegahanKriminalitasState = proxy({
findMany: {
data: null as
| Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}>[]
| null,
page: 1,
@@ -125,11 +93,7 @@ const pencegahanKriminalitasState = proxy({
},
findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}> | null,
loading: false,
async load(id: string) {
@@ -148,6 +112,30 @@ const pencegahanKriminalitasState = proxy({
}
},
},
findFirst: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
omit: { isActive: true };
}> | null,
loading: false,
// findFirst.load()
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.keamanan.pencegahankriminalitas["find-first"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data || null;
} else {
this.data = null;
}
} catch (err) {
console.error("Gagal fetch pencegahan kriminalitas terbaru:", err);
this.data = null;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -213,24 +201,10 @@ const pencegahanKriminalitasState = proxy({
const data = result.data;
pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: data.programKeamanan.nama,
deskripsi: data.programKeamanan.deskripsi,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
judul: data.judul,
deskripsi: data.deskripsi,
deskripsiSingkat: data.deskripsiSingkat,
linkVideo: data.linkVideo,
};
return data;
} else {
@@ -266,40 +240,11 @@ const pencegahanKriminalitasState = proxy({
"Content-Type": "application/json",
},
body: JSON.stringify({
pencegahanKriminalitas: {
programKeamanan: {
nama: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.nama,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
judul: pencegahanKriminalitasState.update.form.judul,
deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
deskripsiSingkat:
pencegahanKriminalitasState.update.form.deskripsiSingkat,
linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
}),
}
);

View File

@@ -31,11 +31,13 @@ const templateForm = z.object({
doctorSign: z.object({
content: z.string().min(1, "Content harus diisi"),
}),
imageId: z.string().min(1, "Image ID harus diisi"),
});
const defaultForm = {
title: "",
content: "",
imageId: "",
introduction: {
content: "",
},
@@ -59,6 +61,7 @@ const defaultForm = {
doctorSign: {
content: "",
},
};
const artikelKesehatanState = proxy({
@@ -112,6 +115,7 @@ const artikelKesehatanState = proxy({
firstaid: true;
mythvsfact: true;
doctorsign: true;
image: true;
};
}>[]
| null,
@@ -159,6 +163,7 @@ const artikelKesehatanState = proxy({
firstaid: true;
mythvsfact: true;
doctorsign: true;
image: true;
};
}> | null,
loading: false,
@@ -213,6 +218,7 @@ const artikelKesehatanState = proxy({
doctorSign: {
content: data.doctorsign.content,
},
imageId: data.imageId,
};
},
async submit() {
@@ -253,6 +259,7 @@ const artikelKesehatanState = proxy({
doctorSign: {
content: artikelKesehatanState.edit.form.doctorSign.content,
},
imageId: artikelKesehatanState.edit.form.imageId,
};
const res = await fetch(

View File

@@ -26,14 +26,6 @@ const templateForm = z.object({
dokumenJadwalKegiatan: z.object({
content: z.string().min(1, "Content minimal 1 karakter"),
}),
pendaftaranJadwalKegiatan: z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
}),
});
const defaultForm = {
@@ -55,15 +47,7 @@ const defaultForm = {
},
dokumenJadwalKegiatan: {
content: "",
},
pendaftaranJadwalKegiatan: {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
},
}
};
const jadwalkegiatanState = proxy({
@@ -116,7 +100,6 @@ const jadwalkegiatanState = proxy({
deskripsijadwalkegiatan: true;
layananjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}>[]
| null,
@@ -161,7 +144,6 @@ const jadwalkegiatanState = proxy({
layananjadwalkegiatan: true;
syaratketentuanjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}> | null,
loading: false,
@@ -209,15 +191,7 @@ const jadwalkegiatanState = proxy({
},
dokumenJadwalKegiatan: {
content: data.dokumenjadwalkegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: data.pendaftaranjadwalkegiatan.name,
tanggal: data.pendaftaranjadwalkegiatan.tanggal,
namaOrangtua: data.pendaftaranjadwalkegiatan.namaOrangtua,
nomor: data.pendaftaranjadwalkegiatan.nomor,
alamat: data.pendaftaranjadwalkegiatan.alamat,
catatan: data.pendaftaranjadwalkegiatan.catatan,
},
}
};
},
async submit() {
@@ -259,20 +233,6 @@ const jadwalkegiatanState = proxy({
content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
.namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
},
};
const res = await fetch(

View File

@@ -0,0 +1,290 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
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 templateForm = z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
});
const defaultForm = {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
};
const pendaftaranJadwalKegiatanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
this.loading = true;
const payload = { ...this.form };
const res = await (ApiFetch.api.kesehatan as any)[
"pendaftaran-jadwal-kegiatan"
].create.post(payload);
if (res.status === 200) {
toast.success("Berhasil menambahkan jadwal kegiatan");
this.resetForm();
await pendaftaranJadwalKegiatanState.findMany.load();
return res.data;
}
} catch (err: any) {
const msg = err?.message || "Terjadi kesalahan saat mengirim data";
toast.error(msg);
console.error("SUBMIT ERROR:", err);
return null;
} finally {
this.loading = false;
}
},
resetForm() {
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
pendaftaranJadwalKegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
pendaftaranJadwalKegiatanState.findMany.page = page;
pendaftaranJadwalKegiatanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["pendaftaran-jadwal-kegiatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pendaftaranJadwalKegiatanState.findMany.data = res.data.data ?? [];
pendaftaranJadwalKegiatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
}
} catch (err) {
console.error(
"Gagal fetch pendaftaran jadwal kegiatan paginated:",
err
);
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
} finally {
pendaftaranJadwalKegiatanState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`
);
if (res.ok) {
const data = await res.json();
pendaftaranJadwalKegiatanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pendaftaranJadwalKegiatanState.delete.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pendaftaran jadwal kegiatan berhasil dihapus"
);
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pendaftaran jadwal kegiatan"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error(
"Terjadi kesalahan saat menghapus pendaftaran jadwal kegiatan"
);
} finally {
pendaftaranJadwalKegiatanState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/pendaftaran-jadwal-kegiatan/${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,
tanggal: data.tanggal,
namaOrangtua: data.namaOrangtua,
nomor: data.nomor,
alamat: data.alamat,
catatan: data.catatan,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(pendaftaranJadwalKegiatanState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pendaftaranJadwalKegiatanState.edit.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
namaOrangtua: this.form.namaOrangtua,
nomor: this.form.nomor,
alamat: this.form.alamat,
catatan: this.form.catatan,
}),
}
);
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 pendaftaran jadwal kegiatan");
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pendaftaran jadwal kegiatan");
}
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pendaftaran jadwal kegiatan"
);
return false;
} finally {
pendaftaranJadwalKegiatanState.edit.loading = false;
}
},
reset() {
pendaftaranJadwalKegiatanState.edit.id = "";
pendaftaranJadwalKegiatanState.edit.form = { ...defaultForm };
},
},
});
export default pendaftaranJadwalKegiatanState;

View File

@@ -354,14 +354,39 @@ const kategoriKegiatan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get();
if (res.status === 200) {
kategoriKegiatan.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriKegiatan.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriKegiatan.findMany.page = page;
kategoriKegiatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriKegiatan.findMany.data = res.data.data ?? [];
kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori kegiatan paginated:", err);
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
} finally {
kategoriKegiatan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KategoriKegiatanGetPayload<{

View File

@@ -352,17 +352,19 @@ const posisiOrganisasi = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
load: async (page = 1, limit?: number, search = "") => {
const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try {
const query: any = { page, limit };
const query: any = { page, limit: appliedLimit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;

View File

@@ -51,7 +51,7 @@ function Login() {
Login
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>

View File

@@ -63,7 +63,7 @@ function Registrasi() {
Registrasi
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -37,6 +37,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent"
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
icon: <IconUsersPlus size={18} stroke={1.8} />,
tooltip: "Ajukan permohonan"
}
];

View File

@@ -198,6 +198,7 @@ function EditBerita() {
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -93,6 +93,7 @@ function DetailBerita() {
h={200}
radius="md"
fit="cover"
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -183,6 +183,7 @@ export default function CreateBerita() {
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -5,7 +5,6 @@ import {
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -87,7 +86,6 @@ function ListBerita({ search }: { search: string }) {
<TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -96,7 +94,7 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
@@ -107,19 +105,6 @@ function ListBerita({ search }: { search: string }) {
{item.kategoriBerita?.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"

View File

@@ -131,6 +131,7 @@ export default function ListImage() {
h={120}
fit="contain"
opacity={0.7}
loading="lazy"
/>
<Text c="dimmed" ta="center">
Belum ada foto yang tersedia

View File

@@ -0,0 +1,178 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Select,
Stack,
TextInput,
Title,
Tooltip
} 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 EditAjukanPermohonan() {
const router = useRouter();
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
const [formData, setFormData] = useState({
nama: stateAjukan.edit.form.nama,
nik: stateAjukan.edit.form.nik,
alamat: stateAjukan.edit.form.alamat,
nomorKk: stateAjukan.edit.form.nomorKk,
kategoriId: stateAjukan.edit.form.kategoriId,
});
useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateAjukan.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
}
} catch (error) {
console.error('Error loading ajukan:', error);
toast.error('Gagal memuat data ajukan');
}
};
loadAjukan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateAjukan.edit.form = {
...stateAjukan.edit.form,
...formData,
};
toast.success('Ajukan berhasil diperbarui!');
router.push('/admin/desa/layanan/ajukan_permohonan');
} catch (error) {
console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Ajukan Permohonan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
required
/>
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
value={formData.nik}
onChange={(e) => setFormData({ ...formData, nik: e.target.value })}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
required
/>
<TextInput
type="number"
label="Nomor KK"
placeholder="Masukkan nomor KK"
value={formData.nomorKk}
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })}
required
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={formData.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateAjukan.edit.form.kategoriId = selected.id;
}
} else {
stateAjukan.edit.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditAjukanPermohonan;

View File

@@ -0,0 +1,172 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailAjukanPermohonan() {
const ajukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
ajukanPermohonanState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
ajukanPermohonanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/desa/layanan/ajukan_permohonan');
}
};
if (!ajukanPermohonanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = ajukanPermohonanState.findUnique.data;
return (
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Surat Keterangan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Nama
</Text>
<Text fz="md" c="dimmed">
{data?.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
NIK
</Text>
<Text fz="md" c="dimmed">
{data?.nik || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Alamat
</Text>
<Text fz="md" c="dimmed">
{data?.alamat || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Nomor KK
</Text>
<Text fz="md" c="dimmed">
{data?.nomorKk || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.kategori.name || '-'}
</Text>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={ajukanPermohonanState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/layanan/ajukan_permohonan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus ajukan permohonan ini?"
/>
</Box>
);
}
export default DetailAjukanPermohonan;

View File

@@ -0,0 +1,155 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} 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 stateLayananDesa from '../../../_state/desa/layananDesa';
function AjukanPermohonan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pelayanan Ajukan Permohonan'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanPermohonan search={search} />
</Box>
);
}
function ListAjukanPermohonan({ search }: { search: string }) {
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = AjukanPermohonanState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Title order={4}>List Ajukan Permohonan</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Alamat</TableTh>
<TableTh style={{ width: '15%' }}>NIK</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nik}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default AjukanPermohonan;

View File

@@ -188,6 +188,7 @@ function EditSuratKeterangan() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
@@ -244,6 +245,7 @@ function EditSuratKeterangan() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -110,6 +110,7 @@ function DetailSuratKeterangan() {
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">
@@ -130,6 +131,7 @@ function DetailSuratKeterangan() {
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">

View File

@@ -170,6 +170,7 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Utama"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
@@ -219,6 +220,7 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Tambahan"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
) : (

View File

@@ -20,7 +20,7 @@ import {
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -124,14 +124,16 @@ function ListSuratKeterangan({ search }: { search: string }) {
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
Detail
</Button>
</TableTd>
</TableTr>

View File

@@ -183,6 +183,7 @@ function EditPenghargaan() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy"
/>
</Box>
)}

View File

@@ -87,6 +87,7 @@ function DetailPenghargaan() {
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">

View File

@@ -152,6 +152,7 @@ function CreatePenghargaan() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}

View File

@@ -21,7 +21,7 @@ import {
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -113,14 +113,16 @@ function ListPenghargaan({ search }: { search: string }) {
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
Detail
</Button>
</TableTd>
</TableTr>

View File

@@ -94,9 +94,11 @@ function ListPengumuman({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</Box>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -60,7 +60,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
<Group justify="space-between">
<Title order={4}>List Kategori Potensi</Title>
<Tooltip label="Tambah Kategori Potensi" withArrow>
<Button
@@ -72,7 +72,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
Tambah Baru
</Button>
</Tooltip>
</Box>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>

View File

@@ -202,6 +202,7 @@ function EditPotensi() {
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -90,6 +90,7 @@ export default function DetailPotensi() {
h={200}
radius="md"
fit="cover"
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -165,6 +165,7 @@ function CreatePotensi() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading='lazy'
/>
</Box>
)}
@@ -195,7 +196,7 @@ function CreatePotensi() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan Potensi
Simpan
</Button>
</Group>
</Stack>

View File

@@ -120,12 +120,14 @@ function ListPotensi({ search }: { search: string }) {
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
Detail
</Button>
</TableTd>
</TableTr>

View File

@@ -241,6 +241,7 @@ function Page() {
height={180}
fit="cover"
radius="sm"
loading='lazy'
/>
<TextInput
label={`Label Gambar ${index + 1}`}

View File

@@ -55,7 +55,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<Paper
bg={colors['blue-button']}
@@ -102,7 +102,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<Paper
bg={colors['blue-button']}
@@ -152,7 +152,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<Paper
bg={colors['blue-button']}
@@ -199,7 +199,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center>
<Paper
bg={colors['blue-button']}
@@ -228,6 +228,7 @@ function Page() {
fit="cover"
radius="md"
style={{ border: '1px solid #ccc' }}
loading='lazy'
/>
</Center>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>

View File

@@ -161,6 +161,7 @@ function EditPerbekelDariMasaKeMasa() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading='lazy'
/>
</Box>
)}

View File

@@ -75,6 +75,7 @@ function DetailPerbekelDariMasa() {
h={150}
radius="md"
fit="cover"
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -131,6 +131,7 @@ function CreatePerbekelDariMasaKeMasa() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading='lazy'
/>
</Box>
)}

View File

@@ -2,7 +2,7 @@
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -82,12 +82,14 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
Detail
</Button>
</TableTd>
</TableTr>

View File

@@ -145,7 +145,7 @@ function ProfilePerbekel() {
{/* Preview */}
<Box mt="sm">
{previewImage ? (
<Image src={previewImage} alt="Preview" w={200} h={200} fit="cover" radius="md" />
<Image loading='lazy' src={previewImage} alt="Preview" w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">

View File

@@ -56,7 +56,7 @@ function Page() {
<Grid>
<GridCol span={12}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
</Center>
</GridCol>
<GridCol span={12}>
@@ -77,6 +77,7 @@ function Page() {
alt="Foto Profil Perbekel"
radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
loading='lazy'
/>
</Center>
<Paper

View File

@@ -164,6 +164,7 @@ function EditPasarDesa() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -95,6 +95,7 @@ function DetailPasarDesa() {
h={120}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -134,6 +134,7 @@ export default function CreatePasarDesa() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}

View File

@@ -208,6 +208,7 @@ export default function EditPegawai() {
fit="cover"
radius="sm"
mt="md"
loading="lazy"
/>
)}
</Box>

View File

@@ -60,7 +60,7 @@ function DetailPegawai() {
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' />
<Image src={statePegawai.findUnique.data?.image?.link} alt='' loading="lazy" />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}

View File

@@ -141,6 +141,7 @@ function CreatePegawai() {
fit="cover"
radius="sm"
mt="md"
loading="lazy"
/>
)}
</Box>

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,87 +10,114 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
function DetailAjukanIdeInofativDesa() {
const state = useProxy(ajukanIdeInovatifState)
const state = useProxy(ajukanIdeInovatifState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
state.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/ajukan-ide-inovatif")
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/ajukan-ide-inovatif");
}
}
};
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = state.findUnique.data;
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>
<Flex justify="space-between" gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>Detail Ajukan Ide Inovatif Desa</Text>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "80%", lg: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Header */}
<Flex justify="space-between" align="center">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Ajukan Ide Inovatif Desa
</Text>
<Tooltip label="Hapus Ide Inovatif" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
}}
variant="light"
radius="md"
size="md"
disabled={state.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.alamat }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Ide Inovatif</Text>
<Text fz={"lg"}>{state.findUnique.data?.namaIde}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Masalah</Text>
<Text fz={"lg"}>{state.findUnique.data?.masalah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Benefit</Text>
<Text fz={"lg"}>{state.findUnique.data?.benefit}</Text>
</Box>
</Stack>
</Paper>
) : null}
{/* Detail Data */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.alamat || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Nama Ide Inovatif</Text>
<Text fz="md" c="dimmed">{data?.namaIde || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Masalah</Text>
<Text fz="md" c="dimmed">{data?.masalah || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Benefit</Text>
<Text fz="md" c="dimmed">{data?.benefit || '-'}</Text>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -99,7 +126,7 @@ function DetailAjukanIdeInofativDesa() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus ajukan ide inovatif ini?'
text="Apakah anda yakin ingin menghapus ajukan ide inovatif ini?"
/>
</Box>
);

View File

@@ -1,8 +1,26 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -15,7 +33,7 @@ function AjukanIdeInovatif() {
<Box>
<HeaderSearch
title='Ajukan Ide Inovatif'
placeholder='pencarian'
placeholder='Cari ide inovatif...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,62 +46,110 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
state.findMany.load()
}, [])
load(page, 10, search)
}, [page, search])
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaIde.toLowerCase().includes(keyword) ||
item.masalah.toLowerCase().includes(keyword) ||
item.benefit.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (!state.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title mb={10} order={3}>List Ajukan Ide Inovatif</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Ide Inovatif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Ide Inovatif</Title>
<Tooltip label="Ajukan Ide Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama</TableTh>
<TableTh style={{ width: '30%' }}>Alamat</TableTh>
<TableTh style={{ width: '30%' }}>Nama Ide Inovatif</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada ide inovatif yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}

View File

@@ -146,6 +146,7 @@ function EditPenghargaan() {
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -60,7 +60,7 @@ function DetailDesaDigital() {
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" />
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
</Box>
<Flex gap={"xs"} mt={10}>
<Button

View File

@@ -1,7 +1,18 @@
'use client'
'use client';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -11,139 +22,168 @@ import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState)
const stateDesaDigital = useProxy(desaDigitalState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateDesaDigital.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
name: '',
deskripsi: '',
imageId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
try {
// Upload the image first
// Upload gambar dulu
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
file,
name: file.name,
});
const uploaded = uploadRes.data?.data
const uploaded = uploadRes.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
return toast.error('Gagal mengunggah gambar');
}
// Set the image ID in the form
stateDesaDigital.create.form.imageId = uploaded.id
// Submit the form
const success = await stateDesaDigital.create.create()
// Set imageId ke form
stateDesaDigital.create.form.imageId = uploaded.id;
// Submit form
const success = await stateDesaDigital.create.create();
if (success) {
resetForm()
router.push("/admin/inovasi/desa-digital-smart-village")
resetForm();
router.push('/admin/inovasi/desa-digital-smart-village');
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
}
};
}
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Desa Digital Smart Village</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Desa Digital Smart Village
</Title>
</Group>
{/* Card */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput
label="Nama Desa Digital Smart Village"
placeholder="Masukkan nama desa digital smart village"
value={stateDesaDigital.create.form.name}
onChange={(val) => {
stateDesaDigital.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Desa Digital Smart Village</Text>}
placeholder="masukkan nama desa digital smart village"
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
required
/>
{/* Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateDesaDigital.create.form.deskripsi}
onChange={(htmlContent) => {
stateDesaDigital.create.form.deskripsi = htmlContent;
onChange={(val) => {
stateDesaDigital.create.form.deskripsi = val;
}}
/>
</Box>
{/* Upload Gambar */}
<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>
<Text fw="bold" fz="sm" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<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>
{/* Preview */}
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,30 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Pagination } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } 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 desaDigitalState from '../../_state/inovasi/desa-digital';
function DesaDigitalSmartVillage() {
@@ -15,8 +32,8 @@ function DesaDigitalSmartVillage() {
return (
<Box>
<HeaderSearch
title='Desa Digital Smart Village'
placeholder='pencarian'
title="Desa Digital Smart Village"
placeholder="Cari inovasi digital desa..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,69 +44,114 @@ function DesaDigitalSmartVillage() {
}
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState)
const router = useRouter()
const state = useProxy(desaDigitalState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Desa Digital Smart Village'
href='/admin/inovasi/desa-digital-smart-village/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Inovasi</TableTh>
<TableTh>Deskripsi Singkat Inovasi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Desa Digital Smart Village</Title>
<Tooltip label="Tambah Inovasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/inovasi/desa-digital-smart-village/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Inovasi</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat Inovasi
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text
fz="sm"
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/inovasi/desa-digital-smart-village/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -13,22 +24,23 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const stateInfoTekno = useProxy(infoTeknoState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateInfoTekno.findUnique.data?.name || '',
deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
imageId: stateInfoTekno.findUnique.data?.imageId || '',
})
});
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
if (!id) return;
const id = params?.id as string;
if (!id) return;
const loadPenghargaan = async () => {
try {
const data = await stateInfoTekno.edit.load(id);
if (data) {
@@ -38,13 +50,11 @@ function EditInfoTeknologiTepatGuna() {
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error("Error loading info teknologi tepat guna:", error);
toast.error("Gagal memuat data info teknologi tepat guna");
console.error('Error loading info teknologi tepat guna:', error);
toast.error('Gagal memuat data info teknologi tepat guna');
}
};
@@ -55,103 +65,127 @@ function EditInfoTeknologiTepatGuna() {
try {
stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
...formData,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
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");
return toast.error('Gagal upload gambar');
}
stateInfoTekno.edit.form.imageId = uploaded.id;
}
await stateInfoTekno.edit.update();
toast.success("Info teknologi tepat guna berhasil diperbarui!");
router.push("/admin/inovasi/info-teknologi-tepat-guna");
toast.success('Info teknologi tepat guna berhasil diperbarui!');
router.push('/admin/inovasi/info-teknologi-tepat-guna');
} catch (error) {
console.error("Error updating info teknologi tepat guna:", error);
toast.error("Terjadi kesalahan saat memperbarui info teknologi tepat guna");
console.error('Error updating info teknologi tepat guna:', error);
toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna');
}
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Info Teknologi Tepat Guna</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol back + title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Info Teknologi Tepat Guna
</Title>
</Group>
{/* Card form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Judul */}
<TextInput
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
required
/>
{/* Upload gambar */}
<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>
<Text fw="bold" fz="sm" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<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>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Deskripsi pakai editor */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -161,7 +195,21 @@ function EditInfoTeknologiTepatGuna() {
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Tombol submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -32,64 +32,106 @@ function DetailInfoTeknologiTepatGuna() {
if (!stateInfoTekno.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = stateInfoTekno.findUnique.data
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 Info Teknologi Tepat Guna</Text>
{stateInfoTekno.findUnique.data ? (
<Paper key={stateInfoTekno.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateInfoTekno.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateInfoTekno.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateInfoTekno.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "70%", lg: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Info Teknologi Tepat Guna
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Teknologi'}
w={150}
h={150}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Buttons */}
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Info Teknologi" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateInfoTekno.findUnique.data) {
setSelectedId(stateInfoTekno.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateInfoTekno.delete.loading || !stateInfoTekno.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={stateInfoTekno.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Info Teknologi" withArrow position="top">
<Button
onClick={() => {
if (stateInfoTekno.findUnique.data) {
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${stateInfoTekno.findUnique.data.id}/edit`);
}
}}
disabled={!stateInfoTekno.findUnique.data}
color={"green"}
color="green"
onClick={() =>
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>

View File

@@ -1,7 +1,18 @@
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -11,76 +22,92 @@ import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState)
const stateInfoTekno = useProxy(infoTeknoState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateInfoTekno.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
name: '',
deskripsi: '',
imageId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
return toast.error('Silahkan pilih file gambar terlebih dahulu');
}
try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
name: file.name,
});
const uploaded = uploadRes.data?.data
const uploaded = uploadRes.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
return toast.error('Gagal upload gambar');
}
// Set the image ID in the form
stateInfoTekno.create.form.imageId = uploaded.id
stateInfoTekno.create.form.imageId = uploaded.id;
// Submit the form
const success = await stateInfoTekno.create.create()
const success = await stateInfoTekno.create.create();
if (success) {
resetForm()
router.push("/admin/inovasi/info-teknologi-tepat-guna")
resetForm();
router.push('/admin/inovasi/info-teknologi-tepat-guna');
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
}
};
}
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Info Teknologi Tepat Guna</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Info Teknologi Tepat Guna
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput
value={stateInfoTekno.create.form.name}
onChange={(val) => {
stateInfoTekno.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder="masukkan nama info teknologi tepat guna"
label="Nama Info Teknologi Tepat Guna"
placeholder="Masukkan nama info teknologi tepat guna"
required
/>
{/* Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateInfoTekno.create.form.deskripsi}
onChange={(htmlContent) => {
@@ -88,62 +115,70 @@ function CreateInfoTeknologiTepatGuna() {
}}
/>
</Box>
{/* Upload Gambar */}
<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>
<Text fw="bold" fz="sm" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" c="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<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>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,30 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } 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 infoTeknoState from '../../_state/inovasi/info-tekno';
function InfoTeknologiTepatGuna() {
@@ -15,8 +32,8 @@ function InfoTeknologiTepatGuna() {
return (
<Box>
<HeaderSearch
title='Info Teknologi Tepat Guna'
placeholder='pencarian'
title="Info Teknologi Tepat Guna"
placeholder="Cari info teknologi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,70 +44,125 @@ function InfoTeknologiTepatGuna() {
}
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState)
const router = useRouter()
const state = useProxy(infoTeknoState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Info Teknologi Tepat Guna'
href='/admin/inovasi/info-teknologi-tepat-guna/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Info Teknologi Tepat Guna</TableTh>
<TableTh>Deskripsi Singkat Info Teknologi Tepat Guna</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Info Teknologi Tepat Guna</Title>
<Tooltip label="Tambah Info Teknologi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>
Nama Info Teknologi
</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '50%' }}>
<Text
lineClamp={1}
truncate
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}

View File

@@ -1,62 +1,118 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconListDetails, IconUsers } from '@tabler/icons-react';
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const router = useRouter();
const pathname = usePathname();
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi",
tooltip: "Lihat daftar kolaborasi inovasi",
icon: <IconListDetails size={18} stroke={1.8} />,
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi",
tooltip: "Kelola mitra kolaborasi",
icon: <IconUsers size={18} stroke={1.8} />,
}
];
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
return (
<Stack>
<Title order={3}>Kolaborasi Inovasi</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>
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 gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Kolaborasi Inovasi
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper biar rapi */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</Stack>
);
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsKolaborasi;
export default LayoutTabsKolaborasi;

View File

@@ -7,11 +7,13 @@ import colors from "@/con/colors";
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title
Title,
Tooltip,
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
@@ -25,33 +27,33 @@ function EditKolaborasiInovasi() {
const params = useParams();
const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || '',
deskripsi: kolaborasiState.update.form.deskripsi || '',
tahun: kolaborasiState.update.form.tahun || '',
slug: kolaborasiState.update.form.slug || '',
kolaborator: kolaborasiState.update.form.kolaborator || '',
name: kolaborasiState.update.form.name || "",
deskripsi: kolaborasiState.update.form.deskripsi || "",
tahun: kolaborasiState.update.form.tahun || "",
slug: kolaborasiState.update.form.slug || "",
kolaborator: kolaborasiState.update.form.kolaborator || "",
});
// Load berita by id saat pertama kali
// Load data
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kolaborasiState.update.load(id); // akses langsung, bukan dari proxy
const data = await kolaborasiState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
tahun: data.tahun || '',
slug: data.slug || '',
kolaborator: data.kolaborator || '',
name: data.name || "",
deskripsi: data.deskripsi || "",
tahun: data.tahun || "",
slug: data.slug || "",
kolaborator: data.kolaborator || "",
});
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
console.error("Error loading kolaborasi:", error);
toast.error("Gagal memuat data kolaborasi inovasi");
}
};
@@ -59,9 +61,7 @@ function EditKolaborasiInovasi() {
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
kolaborasiState.update.form = {
...kolaborasiState.update.form,
name: formData.name,
@@ -71,53 +71,72 @@ function EditKolaborasiInovasi() {
kolaborator: formData.kolaborator,
};
await kolaborasiState.update.submit();
toast.success("Berita berhasil diperbarui!");
toast.success("Kolaborasi inovasi berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
console.error("Error updating kolaborasi:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kolaborasi Inovasi</Title>
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kolaborasi Inovasi
</Title>
</Group>
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi singkat"
required
/>
<TextInput
label="Tahun"
placeholder="Masukkan tahun"
value={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
required
/>
<TextInput
label="Kolaborator"
placeholder="Masukkan nama kolaborator"
value={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>}
placeholder="masukkan kolaborator"
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<Text fw="bold" fz="sm" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -126,7 +145,21 @@ function EditKolaborasiInovasi() {
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,118 +1,143 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
function DetailKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const kolaborasiState = useProxy(kolaborasiInovasiState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
kolaborasiState.findUnique.load(params?.id as string)
}, [])
kolaborasiState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
kolaborasiState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
kolaborasiState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
}
}
};
if (!kolaborasiState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = kolaborasiState.findUnique.data;
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 Kolaborasi Inovasi</Text>
{kolaborasiState.findUnique.data ? (
<Paper key={kolaborasiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Kolaborasi Inovasi</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.tahun}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text>
<Text fz={"lg"} >{kolaborasiState.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Kolaborasi Inovasi
</Text>
{/* Isi detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Kolaborasi Inovasi</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tahun</Text>
<Text fz="md" c="dimmed">{data?.tahun || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Kolaborator</Text>
<Text fz="md" c="dimmed">{data?.kolaborator || '-'}</Text>
</Box>
{/* Tombol aksi */}
<Group gap="sm">
<Tooltip label="Hapus Kolaborasi Inovasi" withArrow position="top">
<Button
color="red"
onClick={() => {
if (kolaborasiState.findUnique.data) {
setSelectedId(kolaborasiState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={kolaborasiState.delete.loading || !kolaborasiState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={kolaborasiState.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Kolaborasi Inovasi" withArrow position="top">
<Button
onClick={() => {
if (kolaborasiState.findUnique.data) {
router.push(`/admin/inovasi/kolaborasi-inovasi/${kolaborasiState.findUnique.data.id}/edit`);
}
}}
disabled={!kolaborasiState.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal konfirmasi hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kolaborasi inovasi ini?'
text="Apakah anda yakin ingin menghapus kolaborasi inovasi ini?"
/>
</Box>
);
}
export default DetailKolaborasiInovasi;
export default DetailKolaborasiInovasi;

View File

@@ -3,7 +3,7 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { YearPickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,10 +11,8 @@ import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState)
const stateCreate = useProxy(kolaborasiInovasiState);
const router = useRouter();
const resetForm = () => {
@@ -24,10 +22,10 @@ function CreateProgramKreatifDesa() {
slug: "",
deskripsi: "",
kolaborator: "",
}
}
};
};
// Generate slug from name
// Generate slug dari name
useEffect(() => {
const { name } = stateCreate.create.form;
if (name) {
@@ -42,67 +40,89 @@ function CreateProgramKreatifDesa() {
const handleSubmit = async () => {
try {
// Submit data kolaborasi inovasi
await stateCreate.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi");
router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
toast.success("Berhasil menambahkan kolaborasi inovasi");
} catch (error) {
console.error("Error creating kolaborasi inovasi:", error);
toast.error("Terjadi kesalahan saat menyimpan data");
}
}
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={3}>Create Kolaborasi Inovasi</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kolaborasi Inovasi
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi"
label={<Text fz="sm" fw="bold">Nama Kolaborasi Inovasi</Text>}
placeholder="Masukkan nama kolaborasi inovasi"
value={stateCreate.create.form.name || ''}
onChange={(val) => stateCreate.create.form.name = val.target.value}
required
/>
<YearPickerInput
clearable
value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null}
label="Tahun"
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Pilih tahun"
onChange={(dateString: string) => {
const year = dateString ? new Date(dateString).getFullYear() : 0;
onChange={(date) => {
const year = date ? new Date(date).getFullYear() : 0;
stateCreate.create.form.tahun = year;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(val) => {
stateCreate.create.form.deskripsi = val;
}}
onChange={(val) => stateCreate.create.form.deskripsi = val}
/>
</Box>
<TextInput
label={<Text fz="sm" fw="bold">Kolaborator</Text>}
placeholder="Masukkan kolaborator"
value={stateCreate.create.form.kolaborator || ''}
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,10 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
@@ -15,8 +32,8 @@ function KolaborasiInovasi() {
return (
<Box>
<HeaderSearch
title='Kolaborasi Inovasi'
placeholder='pencarian'
title="Kolaborasi Inovasi"
placeholder="Cari kolaborasi inovasi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,101 +44,116 @@ function KolaborasiInovasi() {
}
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState)
const listState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const {
data,
loading,
page,
totalPages,
load,
} = listState.findMany
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>{item.slug}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kolaborasi Inovasi</Title>
<Tooltip label="Tambah Kolaborasi Inovasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%' }}>Tahun</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
{item.tahun}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={1} c="dimmed">
{item.slug}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box >
</Box>
);
}

View File

@@ -3,24 +3,41 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import {
IconArrowBack,
IconImageInPicture,
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 EditFoto() {
const state = useProxy(mitraKolaborasi)
function EditMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: state.update.form.name || '',
imageId: state.update.form.imageId || ''
imageId: state.update.form.imageId || '',
});
useEffect(() => {
@@ -32,7 +49,7 @@ function EditFoto() {
if (data) {
setFormData({
name: data.name || '',
imageId: data.imageId || ''
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
@@ -51,7 +68,7 @@ function EditFoto() {
state.update.form = {
...state.update.form,
name: formData.name,
imageId: formData.imageId
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
@@ -60,7 +77,7 @@ function EditFoto() {
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal upload gambar');
}
state.update.form.imageId = uploaded.id;
}
@@ -74,70 +91,114 @@ function EditFoto() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Mitra
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Mitra</Title>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama */}
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
label="Nama Mitra"
placeholder="Masukkan nama mitra"
value={formData.name}
onChange={(e) =>
(formData.name = e.target.value)
}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
{/* Upload Foto */}
<Box>
<Text>Upload Foto</Text>
<Text fw="bold" fz="sm" mb={6}>
Upload Foto
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</div>
</Stack>
</Group>
</Dropzone>
{/* Preview Foto */}
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
<Center w={200} h={200} bg="gray.1" mt="sm" style={{ borderRadius: 8 }}>
<IconImageInPicture size={48} color="#868e96" />
</Center>
)}
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
@@ -145,4 +206,4 @@ function EditFoto() {
);
}
export default EditFoto;
export default EditMitraKolaborasi;

View File

@@ -2,7 +2,18 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -10,27 +21,24 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const state = useProxy(mitraKolaborasi)
function CreateMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
state.create.form = {
name: "",
imageId: "",
name: '',
imageId: '',
};
setPreviewImage(null)
setFile(null)
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
@@ -40,92 +48,110 @@ function CreateFoto() {
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi")
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Mitra Kolaborasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Mitra</Title>
{/* Card Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama Mitra */}
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.target.value;
}}
label="Nama Mitra"
placeholder="Masukkan nama mitra"
value={state.create.form.name || ''}
onChange={(e) => (state.create.form.name = e.target.value)}
required
/>
{/* Upload Image */}
<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>
<Text fw="bold" fz="sm" mb={6}>
Gambar Mitra
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<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>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
@@ -133,4 +159,4 @@ function CreateFoto() {
);
}
export default CreateFoto;
export default CreateMitraKolaborasi;

View File

@@ -1,13 +1,31 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit, IconSearch, IconX, IconPlus } 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 mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
@@ -16,8 +34,8 @@ function MitraKolaborasi() {
return (
<Box>
<HeaderSearch
title='Mitra Kolaborasi'
placeholder='pencarian'
title="Mitra Kolaborasi"
placeholder="Cari nama mitra..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,159 +46,169 @@ function MitraKolaborasi() {
}
function ListMitraKolaborasi({ search }: { search: string }) {
const listState = useProxy(mitraKolaborasi)
const listState = useProxy(mitraKolaborasi);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleHapus = () => {
if (selectedId) {
mitraKolaborasi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
mitraKolaborasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/inovasi/kolaborasi-inovasi');
}
}
};
const {
data,
loading,
page,
totalPages,
load,
} = listState.findMany
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data mitra kolaborasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box style={{
width: 100,
height: 100,
position: 'relative',
overflow: 'hidden',
borderRadius: 4
}}>
<Image
src={item.image?.link || ''}
alt={item.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center'
}}
/>
</Box>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={mitraKolaborasi.delete.loading || !item}
color={"red"}
>
<IconX size={20} />
</Button>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
router.push(`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`);
}
}}
disabled={!item}
color={"green"}
>
<IconEdit size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Mitra Kolaborasi</Title>
<Tooltip label="Tambah Mitra Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh style={{ width: '30%' }}>Nama Mitra</TableTh>
<TableTh style={{ width: '25%' }}>Image</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box
w={70}
h={70}
>
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
/>
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd>
<Tooltip label="Hapus Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="red"
disabled={mitraKolaborasi.delete.loading || !item}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={16} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Edit Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="green"
disabled={!item}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus mitra kolaborasi ini?'
text="Apakah anda yakin ingin menghapus mitra kolaborasi ini?"
/>
</Box >
</Box>
);
}

View File

@@ -1,72 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import {
ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title,
Tooltip
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import {
IconFileText,
IconListDetails,
IconMessage,
IconAlertCircle
} from '@tabler/icons-react';
function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Administrasi Online",
value: "administrasionline",
href: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
label: "Jenis Layanan",
value: "jenislayanan",
href: "/admin/inovasi/layanan-online-desa/jenis-layanan"
},
{
label: "Pengaduan Masyarakat",
value: "pengaduanmasyarakat",
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat"
},
{
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const router = useRouter();
const pathname = usePathname();
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
// ✅ Tambahin icon + tooltip biar konsisten sama versi berita
const tabs = [
{
label: "Administrasi Online",
value: "administrasionline",
href: "/admin/inovasi/layanan-online-desa/administrasi-online",
icon: <IconFileText size={18} stroke={1.8} />,
tooltip: "Kelola administrasi online desa"
},
{
label: "Jenis Layanan",
value: "jenislayanan",
href: "/admin/inovasi/layanan-online-desa/jenis-layanan",
icon: <IconListDetails size={18} stroke={1.8} />,
tooltip: "Daftar jenis layanan desa"
},
{
label: "Pengaduan Masyarakat",
value: "pengaduanmasyarakat",
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat",
icon: <IconMessage size={18} stroke={1.8} />,
tooltip: "Laporan pengaduan masyarakat"
},
{
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan",
icon: <IconAlertCircle size={18} stroke={1.8} />,
tooltip: "Kategori/jenis pengaduan masyarakat"
}
];
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
return (
<Stack>
<Title order={3}>Layanan Online Desa</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>
);
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 gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>
Layanan Online Desa
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal biar gak overflow */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsLayananOnlineDesa;
export default LayoutTabsLayananOnlineDesa;

View File

@@ -1,92 +1,111 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailAdministrasiOnline() {
const beritaState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const stateAdminOnline = useProxy(layananonlineDesa.administrasiOnline);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
beritaState.administrasiOnline.findUnique.load(params?.id as string)
}, [])
stateAdminOnline.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
beritaState.administrasiOnline.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/administrasi-online")
stateAdminOnline.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/layanan-online-desa/administrasi-online");
}
}
};
if (!beritaState.administrasiOnline.findUnique.data) {
if (!stateAdminOnline.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateAdminOnline.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box py={10}>
<Group justify='space-between' align='center' w={{ base: "100%", md: "50%" }} mb={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
>
Kembali
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Administrasi Online</Text>
<Button
onClick={() => {
if (beritaState.administrasiOnline.findUnique.data) {
setSelectedId(beritaState.administrasiOnline.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={beritaState.administrasiOnline.delete.loading || !beritaState.administrasiOnline.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{beritaState.administrasiOnline.findUnique.data ? (
<Paper key={beritaState.administrasiOnline.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Layanan</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.jenisLayanan?.nama}</Text>
</Box>
</Stack>
</Paper>
) : null}
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Group>
{/* Konten Detail */}
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Administrasi Online
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data.nomorTelepon || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Layanan</Text>
<Text fz="md" c="dimmed">{data.jenisLayanan?.nama || '-'}</Text>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -95,10 +114,10 @@ function DetailAdministrasiOnline() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
text="Apakah Anda yakin ingin menghapus administrasi online ini?"
/>
</Box>
);
}
export default DetailAdministrasiOnline;
export default DetailAdministrasiOnline;

View File

@@ -1,9 +1,24 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -15,8 +30,8 @@ function AdministrasiOnline() {
return (
<Box>
<HeaderSearch
title='Administrasi Online'
placeholder='pencarian'
title="Administrasi Online"
placeholder="Cari nama layanan, alamat, atau nomor telepon..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,69 +42,103 @@ function AdministrasiOnline() {
}
function ListAdministrasiOnline({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.administrasiOnline)
const state = useProxy(layananonlineDesa.administrasiOnline);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, search);
}, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
const filteredData = data || [];
if (loading || !data) {
return <Skeleton h={500} />;
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Administrasi Online</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Layanan</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4}>Daftar Administrasi Online</Title>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Alamat</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.alamat}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.nomorTelepon || '-'}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/administrasi-online/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data administrasi online yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -32,60 +32,88 @@ function DetailJenisLayanan() {
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = state.findUnique.data
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 Jenis Layanan</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }}></Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Jenis Layanan
</Text>
{/* Detail isi */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
{/* Tombol aksi */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Jenis Layanan" withArrow position="top">
<Button
color="red"
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id)
setModalHapus(true)
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={state.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Jenis Layanan" withArrow position="top">
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -94,7 +122,7 @@ function DetailJenisLayanan() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus jenis layanan ini?'
text='Apakah Anda yakin ingin menghapus jenis layanan ini?'
/>
</Box>
);

View File

@@ -2,7 +2,17 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -10,7 +20,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() {
const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan)
const statePasar = useProxy(layananonlineDesa.jenisLayanan);
useEffect(() => {
statePasar.findMany.load();
@@ -18,51 +28,81 @@ function CreateJenisLayanan() {
const resetForm = () => {
statePasar.create.form = {
nama: "",
deskripsi: "",
nama: '',
deskripsi: '',
};
}
};
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
}
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
};
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Layanan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Layanan</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw="bold" fz="sm">Nama Jenis Layanan</Text>}
placeholder="Masukkan nama jenis layanan"
required
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw="bold" fz="sm">Deskripsi</Text>}
placeholder="Masukkan deskripsi"
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Layanan</Text>}
placeholder='Masukkan nama jenis layanan'
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,23 +1,39 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisLayanan() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jenis Layanan'
placeholder='pencarian'
title="Jenis Layanan"
placeholder="Cari jenis layanan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,59 +44,115 @@ function JenisLayanan() {
}
function ListJenisLayanan({ search }: { search: string }) {
const stateList = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
const stateList = useProxy(layananonlineDesa.jenisLayanan);
const router = useRouter();
const { data, page, totalPages, loading, load } = stateList.findMany;
useShallowEffect(() => {
stateList.findMany.load()
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = data || [];
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!stateList.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Layanan'
href='/admin/inovasi/layanan-online-desa/jenis-layanan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Layanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.deskripsi}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Layanan</Title>
<Tooltip label="Tambah Jenis Layanan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/layanan-online-desa/jenis-layanan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Jenis Layanan</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd style={{ width: '40%' }}>
<Text fz="sm" c="dimmed" lineClamp={2}>
{item.deskripsi || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada jenis layanan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}

View File

@@ -2,7 +2,16 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -70,24 +79,55 @@ function EditJenisPengaduan() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header + tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pengaduan</Title>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -2,7 +2,16 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -10,7 +19,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() {
const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan)
const state = useProxy(layananonlineDesa.jenisPengaduan);
useEffect(() => {
state.findMany.load();
@@ -18,42 +27,64 @@ function CreateJenisPengaduan() {
const resetForm = () => {
state.create.form = {
nama: "",
nama: '',
};
}
};
const handleSubmit = async () => {
await state.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan")
}
router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
};
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pengaduan</Title>
<TextInput
value={state.create.form.nama}
onChange={(val) => {
state.create.form.nama = val.target.value;
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
value={state.create.form.nama || ''}
onChange={(e) => (state.create.form.nama = e.target.value)}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,25 +1,40 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisPengaduan() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jenis Pengaduan'
placeholder='pencarian'
title="Jenis Pengaduan"
placeholder="Cari nama jenis pengaduan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,87 +45,138 @@ function JenisPengaduan() {
}
function ListJenisPengaduan({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.jenisPengaduan)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const state = useProxy(layananonlineDesa.jenisPengaduan);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const { data, page, totalPages, loading, load, } = state.findMany;
useShallowEffect(() => {
state.findMany.load()
}, [])
load(page, 10, search);
}, [page, search]);
const handleHapus = async () => {
if (selectedId) {
await state.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
setModalHapus(false);
setSelectedId(null);
}
}
};
const filteredData = data || []
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Pengaduan'
href='/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Pengaduan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Pengaduan</Title>
<Tooltip label="Tambah Jenis Pengaduan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Jenis Pengaduan</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`
)
}
>
Edit
</Button>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
Hapus
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada jenis pengaduan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,7 +1,6 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -11,102 +10,143 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailPengaduanMasyarakat() {
const pengaduanState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const pengaduanState = useProxy(layananonlineDesa);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string)
}, [])
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat")
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat");
}
}
};
if (!pengaduanState.pengaduanMasyarakat.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = pengaduanState.pengaduanMasyarakat.findUnique.data;
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>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Pengaduan Masyarakat</Text>
<Button
onClick={() => {
if (pengaduanState.pengaduanMasyarakat.findUnique.data) {
setSelectedId(pengaduanState.pengaduanMasyarakat.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={pengaduanState.pengaduanMasyarakat.delete.loading || !pengaduanState.pengaduanMasyarakat.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{pengaduanState.pengaduanMasyarakat.findUnique.data ? (
<Paper key={pengaduanState.pengaduanMasyarakat.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.judulPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi Kejadian</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.lokasiKejadian}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.deskripsiPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.jenisPengaduan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={pengaduanState.pengaduanMasyarakat.findUnique.data?.image?.link} alt="gambar" />
</Box>
</Stack>
</Paper>
) : null}
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Judul Halaman */}
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pengaduan Masyarakat
</Text>
{/* Isi Data */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Email</Text>
<Text fz="md" c="dimmed">{data?.email || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data?.nomorTelepon || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">NIK</Text>
<Text fz="md" c="dimmed">{data?.nik || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Judul Pengaduan</Text>
<Text fz="md" c="dimmed">{data?.judulPengaduan || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Lokasi Kejadian</Text>
<Text fz="md" c="dimmed">{data?.lokasiKejadian || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Pengaduan</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsiPengaduan || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Pengaduan</Text>
<Text fz="md" c="dimmed">{data?.jenisPengaduan?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Pengaduan"
w={120}
h={120}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Pengaduan" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -115,10 +155,10 @@ function DetailPengaduanMasyarakat() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
text="Apakah anda yakin ingin menghapus pengaduan masyarakat ini?"
/>
</Box>
);
}
export default DetailPengaduanMasyarakat;
export default DetailPengaduanMasyarakat;

View File

@@ -1,6 +1,24 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
@@ -16,7 +34,7 @@ function PengaduanMasyarakat() {
<Box>
<HeaderSearch
title='Pengaduan Masyarakat'
placeholder='pencarian'
placeholder='Cari nama, email, atau nomor telepon...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,7 +45,7 @@ function PengaduanMasyarakat() {
}
function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat)
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat);
const router = useRouter();
const {
data,
@@ -38,58 +56,89 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, search);
}, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.email.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (loading || !data) {
return <Skeleton h={500} />;
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Pengaduan Masyarakat</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Email</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pengaduan Masyarakat</Title>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Email</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.email}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.nomorTelepon}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Tooltip label="Lihat detail pengaduan" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pengaduan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -19,36 +29,33 @@ interface FormProgramKreatif {
icon: string;
}
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)
const params = useParams()
const stateProgramKreatif = useProxy(programKreatifState);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
})
});
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateProgramKreatif.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
@@ -57,15 +64,13 @@ function EditProgramKreatifDesa() {
});
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
console.error('Error loading program kreatif:', error);
toast.error('Gagal memuat data program kreatif');
}
}
};
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
@@ -75,49 +80,57 @@ function EditProgramKreatifDesa() {
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
}
};
await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa");
router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) {
console.error("Error updating program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
console.error('Error updating program kreatif:', error);
toast.error('Gagal memuat data program kreatif');
}
}
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={3}>Edit Program Kreatif Desa</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Program Kreatif Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Program Kreatif Desa"
placeholder="Masukkan nama program kreatif desa"
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Deskripsi Singkat Program Kreatif Desa"
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="masukkan deskripsi singkat program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -126,19 +139,34 @@ function EditProgramKreatifDesa() {
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramKreatif.update.form.icon = value;
}}
/>
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,8 +10,6 @@ import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState)
@@ -34,74 +32,104 @@ function DetailProgramKreatifDesa() {
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</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 Program Kreatif Desa</Text>
const data = stateProgramKreatif.findUnique.data
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
return (
<Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Kreatif Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Program Kreatif Desa</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{stateProgramKreatif.findUnique.data?.icon && (
<Text fz="lg" fw="bold">Ikon Program Kreatif Desa</Text>
{data?.icon ? (
<IconMapper
name={stateProgramKreatif.findUnique.data?.icon as IconKey}
name={data.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Flex gap="sm" mt={10}>
<Tooltip label="Hapus Program Kreatif Desa" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
setSelectedId(stateProgramKreatif.findUnique.data.id);
if (data) {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data}
color={"red"}
disabled={stateProgramKreatif.delete.loading || !data}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Program Kreatif Desa" withArrow position="top">
<Button
color="green"
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`);
if (data) {
router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
}
}}
disabled={!stateProgramKreatif.findUnique.data}
color={"green"}
disabled={!data}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Flex>
</Stack>
</Paper>
</Stack>
@@ -112,7 +140,7 @@ function DetailProgramKreatifDesa() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?"
text="Apakah Anda yakin ingin menghapus program kreatif desa ini?"
/>
</Box>
);

View File

@@ -1,6 +1,16 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -8,9 +18,8 @@ import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../../../_com/selectIcon';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState)
const stateCreate = useProxy(programKreatifState);
const router = useRouter();
const resetForm = () => {
@@ -19,48 +28,90 @@ function CreateProgramKreatifDesa() {
slug: "",
deskripsi: "",
icon: "",
}
}
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
router.push("/admin/inovasi/program-kreatif-desa");
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Program Kreatif Desa</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol kembali */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Program Kreatif Desa
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
label={<Text fw="bold" fz="sm">Nama Program Kreatif Desa</Text>}
placeholder="Masukkan nama program kreatif desa"
value={stateCreate.create.form.name || ""}
onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<TextInput
label={<Text fw="bold" fz="sm">Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={stateCreate.create.form.slug || ""}
onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Program Kreatif Desa
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
{/* Tombol Submit */}
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,31 +1,64 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconCash, IconChristmasTreeFilled, IconClipboard, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconHospital, IconScale, IconSchool, IconSearch, IconShieldFilled, IconShoppingCart, IconTrash, IconTree, IconTrendingUp, IconTruck } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboard,
IconDeviceImac,
IconDroplet,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconPlus,
IconRecycle,
IconScale,
IconSchool,
IconSearch,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrash,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruck,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKreatifState from '../../_state/inovasi/program-kreatif';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kreatif Desa'
placeholder='pencarian'
title="Program Kreatif Desa"
placeholder="Cari program kreatif..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -36,23 +69,15 @@ function ProgramKreatifDesa() {
}
function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState)
const { data, loading, page, totalPages, load } = listState.findMany
const listState = useProxy(programKreatifState);
const { data, loading, page, totalPages, load } = listState.findMany;
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
load(page, 10, search);
}, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -74,26 +99,40 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital
layananKesehatan: IconHospital,
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={650} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Paper p="md" radius="md" shadow="sm" withBorder>
<Stack>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Tooltip label="Tambah Program Kreatif Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Table highlightOnHover striped>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
@@ -104,27 +143,52 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text>
<Text ta="center" c="dimmed" py="lg">
Tidak ada data program kreatif desa yang tersedia
</Text>
</Stack>
</Paper>
</Box >
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Paper
withBorder
bg={colors['white-1']}
p="lg"
shadow="md"
radius="md"
h={{ base: 'auto', md: 650 }}
>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Tooltip label="Tambah Program Kreatif Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<Table highlightOnHover striped >
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '20%' }}>
<Text lineClamp={1} fw={"bold"} fz="sm">Nama Program Kreatif Desa</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
@@ -132,9 +196,19 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd>
<TableTd style={{ width: '10%' }}>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Box w={150}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.slug}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '10%', textAlign: 'center' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
@@ -142,8 +216,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}>
<IconDeviceImac size={25} />
<Button
size="xs"
radius="md"
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)
}
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
</Button>
</TableTd>
</TableTr>
@@ -157,14 +240,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default ProgramKreatifDesa;

View File

@@ -186,7 +186,7 @@ function EditKeamananLingkungan() {
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
<Image alt="" src={previewImage} w={200} h={200} loading="lazy"/>
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />

View File

@@ -77,6 +77,7 @@ function DetailKeamananLingkungan() {
w={{ base: 150, md: 490 }}
src={data?.image?.link}
alt="gambar keamanan lingkungan"
loading="lazy"
/>
</Box>

View File

@@ -164,6 +164,7 @@ function CreateKeamananLingkungan() {
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -1,29 +1,36 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} 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 EditKontakItem() {
const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratItem)
const params = useParams()
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
const params = useParams();
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kontakState.update.form.nama || '',
imageId: kontakState.update.form.imageId || '',
nomorTelepon: kontakState.update.form.nomorTelepon || '',
})
icon: kontakState.update.form.icon || '',
});
useEffect(() => {
const loadKontakDarurat = async () => {
@@ -35,17 +42,13 @@ function EditKontakItem() {
if (data) {
setFormData({
name: data.nama || '',
imageId: data.imageId || '',
nomorTelepon: data.nomorTelepon || '',
icon: data.icon || '',
});
if (data?.image?.link) {
setPreviewUtama(data.image.link);
}
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
console.error('Error loading kontak darurat:', error);
toast.error('Gagal memuat data kontak darurat');
}
};
@@ -57,115 +60,84 @@ function EditKontakItem() {
kontakState.update.form = {
...kontakState.update.form,
nama: formData.name,
imageId: formData.imageId,
nomorTelepon: formData.nomorTelepon,
}
if (fileUtama) {
const res = await ApiFetch.api.fileStorage.create.post({ file: fileUtama, name: fileUtama.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakState.update.form.imageId = uploaded.id;
}
icon: formData.icon,
};
await kontakState.update.update();
toast.success("Kontak Darurat berhasil diperbarui!");
router.push("/admin/keamanan/kontak-darurat/kontak-darurat-item");
toast.success('Kontak Darurat berhasil diperbarui!');
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
console.error('Error updating kontak darurat:', error);
toast.error('Terjadi kesalahan saat memperbarui kontak darurat');
}
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kontak Darurat Item
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kontak Darurat</Title>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Kontak"
placeholder="Masukkan nama kontak"
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak</Text>}
placeholder='Masukkan nama Kontak'
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nomor Telepon"
placeholder="Masukkan nomor telepon"
value={formData.nomorTelepon}
onChange={(val) => {
setFormData({ ...formData, nomorTelepon: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon</Text>}
placeholder='Masukkan nomor telepon'
onChange={(e) => setFormData({ ...formData, nomorTelepon: e.target.value })}
required
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(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 */}
{previewUtama && (
<Box mt="sm">
<Image
src={previewUtama}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
kontakState.update.form.icon = value;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,11 +1,12 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -62,48 +63,31 @@ function DetailKontakDarurat() {
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Kontak Darurat
Detail Kontak Darurat Item
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
{/* Judul */}
<Box>
<Text fz="lg" fw="bold">Judul Kontak Darurat</Text>
<Text fz="lg" fw="bold">Judul Kontak Darurat Item</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Judul Kontak Darurat</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
<Text fz="lg" fw="bold">Nomor Telepon Kontak Darurat Item</Text>
<Text fz="md" c="dimmed">{data?.nomorTelepon || '-'}</Text>
</Box>
{/* Gambar Utama */}
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
<Image
w={{ base: 150, md: 490 }}
src={data?.image?.link}
alt="gambar kontak darurat"
/>
</Box>
{/* Kontak Item */}
<Box>
<Text fz="lg" fw="bold">Kontak</Text>
<Stack>
<Box>
<Text fz="md" fw="bold">{data.nama}</Text>
<Text fz="md" c="dimmed">{data.nomorTelepon}</Text>
{data.image?.link && (
<Image
w={{ base: 120, md: 200 }}
src={data.image.link}
alt={`gambar kontak ${data.nama}`}
/>
)}
</Box>
</Stack>
<Text fz={"lg"} fw={"bold"}>Ikon Kontak Darurat</Text>
{data?.icon && (
<IconMapper
name={data?.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
)}
</Box>
{/* Aksi */}
@@ -126,7 +110,7 @@ function DetailKontakDarurat() {
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/keamanan/kontak-darurat/${data.id}/edit`)}
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-item/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -1,59 +1,34 @@
'use client'
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateKontakItem() {
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
const router = useRouter();
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
const resetForm = () => {
kontakState.create.form = {
nama: '',
imageId: '',
icon: '',
nomorTelepon: '',
};
setPreviewUtama(null);
setFileUtama(null);
};
const handleSubmit = async () => {
if (!fileUtama) {
return toast.warn('Pilih file gambar kategori terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file: fileUtama,
name: fileUtama.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengupload gambar kategori');
}
kontakState.create.form.imageId = uploaded.id;
await kontakState.create.create();
resetForm();
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
@@ -109,67 +84,11 @@ function CreateKontakItem() {
required
/>
{/* Upload Gambar Kategori */}
<Box>
<Text fz="sm" fw="bold">
Gambar Kontak Darurat
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
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>
{previewUtama && (
<Box mt="sm">
<Image
src={previewUtama}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
<Text fz={"sm"} fw={"bold"}>Ikon Kontak Darurat Item</Text>
<SelectIconProgram onChange={(value) => kontakState.create.form.icon = value} />
</Box>
{/* Tombol Submit */}
<Group justify="right">
<Button

View File

@@ -1,30 +1,24 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { IconKey } from "@/app/admin/(dashboard)/_com/iconMap";
import SelectIconProgramEdit from "@/app/admin/(dashboard)/_com/selectIconEdit";
import kontakDarurat from "@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
Box,
Button,
Center,
Group,
Image,
MultiSelect,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Tooltip
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import {
IconArrowBack,
IconImageInPicture,
IconPhoto,
IconUpload,
IconX,
IconArrowBack
} from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -32,65 +26,52 @@ import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditKontakDaruratKeamanan() {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const params = useParams();
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kontakState.update.form.nama || "",
imageId: kontakState.update.form.imageId || "",
icon: kontakState.update.form.icon || "",
kategoriId: kontakState.update.form.kategoriId || [],
});
// Load data
useEffect(() => {
kontakDarurat.kontakDaruratItem.findMany.load();
const loadKontakDarurat = async () => {
const id = params?.id as string;
if (!id) return;
const loadData = async () => {
try {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || "",
imageId: data.imageId || "",
kategoriId: data.kategoriId || [],
});
if (data?.image?.link) setPreviewUtama(data.image.link);
setIsLoading(true);
await kontakDarurat.kontakDaruratItem.findMany.load();
const id = params?.id as string;
if (id) {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || "",
icon: data.icon || "",
kategoriId: data.kategoriId || [],
});
}
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
console.error("Error loading data:", error);
toast.error("Gagal memuat data");
} finally {
setIsLoading(false);
}
};
loadKontakDarurat();
loadData();
}, [params?.id]);
// Handle submit
const handleSubmit = async () => {
try {
kontakState.update.form = {
...kontakState.update.form,
nama: formData.name,
imageId: formData.imageId,
icon: formData.icon,
kategoriId: formData.kategoriId,
};
if (fileUtama) {
const res = await ApiFetch.api.fileStorage.create.post({
file: fileUtama,
name: fileUtama.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error("Gagal upload gambar utama");
kontakState.update.form.imageId = uploaded.id;
}
await kontakState.update.update();
toast.success("Kontak Darurat berhasil diperbarui!");
router.push("/admin/keamanan/kontak-darurat");
@@ -142,65 +123,31 @@ function EditKontakDaruratKeamanan() {
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kontak Item</Text>}
placeholder='Pilih kontak item'
placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"}
data={
kontakDarurat.kontakDaruratItem.findMany.data?.map((v) => ({
value: v.id, // Make sure this is using the ID
label: v.nama
})) || []
Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
value: v.id,
label: v.nama
}))
: []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
disabled={isLoading}
/>
{/* Gambar utama */}
<Box>
<Text fz="sm" fw="bold">
Masukkan Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(URL.createObjectURL(selectedFile));
}
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
kontakState.update.form.icon = value;
}}
onReject={() => toast.error("File tidak valid.")}
maxSize={5 * 1024 ** 2}
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>
{previewUtama ? (
<Image alt="" src={previewUtama} w={200} h={200} mt="sm" />
) : (
<Center w={200} h={200} bg={"gray"} mt="sm">
<IconImageInPicture />
</Center>
)}
/>
</Box>
{/* Submit */}

View File

@@ -1,11 +1,12 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -17,7 +18,7 @@ function DetailKontakDaruratKeamanan() {
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
useShallowEffect(() => {
kontakDarurat.kontakDaruratItem.findUnique.load(params?.id as string);
kontakDarurat.kontakDaruratItem.findMany.load();
kontakState.findUnique.load(params?.id as string);
}, []);
@@ -74,14 +75,15 @@ function DetailKontakDaruratKeamanan() {
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
{/* Gambar Utama */}
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
<Image
w={{ base: 150, md: 490 }}
src={data?.image?.link}
alt="gambar kontak darurat"
/>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{data?.icon && (
<IconMapper
name={data?.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
)}
</Box>
{/* Kontak Items */}
@@ -92,11 +94,11 @@ function DetailKontakDaruratKeamanan() {
<Box key={index}>
<Text fz="md" fw="bold">{item.kontakItem.nama}</Text>
<Text fz="md" c="dimmed">{item.kontakItem.nomorTelepon}</Text>
{item.kontakItem.image?.link && (
<Image
w={{ base: 120, md: 200 }}
src={item.kontakItem.image.link}
alt={`gambar kontak`}
{item.kontakItem.icon && (
<IconMapper
name={item.kontakItem.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
)}
</Box>

View File

@@ -1,35 +1,28 @@
'use client'
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
MultiSelect,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateKontakDaruratKeamanan() {
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const router = useRouter();
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
useShallowEffect(() => {
kontakDarurat.kontakDaruratItem.findMany.load();
}, []);
@@ -37,29 +30,12 @@ function CreateKontakDaruratKeamanan() {
const resetForm = () => {
kontakState.create.form = {
nama: '',
imageId: '',
icon: '',
kategoriId: []
};
setPreviewUtama(null);
setFileUtama(null);
};
const handleSubmit = async () => {
if (!fileUtama) {
return toast.warn('Pilih file gambar kategori terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file: fileUtama,
name: fileUtama.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengupload gambar kategori');
}
kontakState.create.form.imageId = uploaded.id;
await kontakState.create.create();
resetForm();
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan');
@@ -105,64 +81,9 @@ function CreateKontakDaruratKeamanan() {
required
/>
{/* Upload Gambar Kategori */}
<Box>
<Text fz="sm" fw="bold">
Masukkan Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
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>
{previewUtama && (
<Box mt="sm">
<Image
src={previewUtama}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
<Text fz={"sm"} fw={"bold"}>Ikon Kontak Darurat</Text>
<SelectIconProgram onChange={(value) => kontakState.create.form.icon = value} />
</Box>
<MultiSelect

View File

@@ -148,12 +148,16 @@ function EditLaporanPublik() {
required
/>
<TextInput
value={formData.kronologi}
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
placeholder="Masukkan kronologi laporan publik"
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Kronologi Laporan Publik</Text>
<EditEditor
value={formData.kronologi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, kronologi: htmlContent }));
stateLaporan.edit.form.kronologi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>

View File

@@ -73,7 +73,7 @@ function DetailLaporanPublik() {
Detail Laporan Publik
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul Laporan Publik</Text>
@@ -96,12 +96,32 @@ function DetailLaporanPublik() {
<Box>
<Text fz="lg" fw="bold">Status</Text>
<Text fz="md" c="dimmed">{data.status || '-'}</Text>
<Box
style={{
display: 'inline-block',
padding: '4px 12px',
borderRadius: '16px',
backgroundColor:
data.status === 'Selesai' ? '#94EF95FF' :
data.status === 'Proses' ? '#F1D295FF' :
'#F38E8EFF',
color:
data.status === 'Selesai' ? '#01BA01FF' :
data.status === 'Proses' ? '#B67A00FF' :
'#AE1700FF',
fontWeight: 900,
fontSize: '0.75rem',
textAlign: 'center',
minWidth: '80px',
}}
>
{data.status || '-'}
</Box>
</Box>
<Box>
<Text fz="lg" fw="bold">Kronologi</Text>
<Text fz="md" c="dimmed">{data.kronologi || '-'}</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kronologi || '' }} />
</Box>
<Box>
@@ -109,7 +129,6 @@ function DetailLaporanPublik() {
{data.penanganan?.length ? (
data.penanganan.map((item, index) => (
<Box key={index} pl="sm">
<Text fz="md" fw="bold">Deskripsi Penanganan</Text>
<Text
fz="md"
c="dimmed"

Some files were not shown because too many files have changed in this diff Show More