Compare commits

..

4 Commits

Author SHA1 Message Date
8d15563f15 Sinkronisasi UI & API Admin - User Submenu Data Kesehatan Warga
-Dibagian Tanggal Gak Auto Ngambil Tanggal Yang Udah Dipakai
-Dibagian fasilitas kesehatan : data dokter dan tarif rencananya mau pakai select
2025-08-15 14:07:56 +08:00
d7a592c635 Fix UI & API Admin Menu Kesehatan, Submenu Data Kesehatan Warga Bagian ChartBar 2025-08-14 20:47:07 +08:00
5e137ba658 Fix Admin Submenu Posyandu, Menu Kesehatan, dan Sinkronisasi UI & API Admin - User Submenu Posyandu 2025-08-14 11:48:57 +08:00
c99416c7f8 Fix FileInput dengan Dropzone 2025-08-14 10:24:03 +08:00
97 changed files with 5769 additions and 1816 deletions

View File

@@ -911,26 +911,56 @@ model PendaftaranJadwalKegiatan {
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= // // ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
model DataKematian_Kelahiran { model DataKematian_Kelahiran {
id String @id @default(cuid()) id String @id @default(cuid())
tahun String kematian Kematian @relation(fields: [kematianId], references: [id])
kematianKasar String kematianId String
kematianBayi String kelahiran Kelahiran @relation(fields: [kelahiranId], references: [id])
kelahiranKasar String kelahiranId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
}
model Kelahiran {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
}
model Kematian {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
penyebab String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
} }
// ========================================= GRAFIK KEPUASAN ========================================= // // ========================================= GRAFIK KEPUASAN ========================================= //
model GrafikKepuasan { model GrafikKepuasan {
id String @id @default(cuid()) id String @id @default(cuid())
label String nama String
jumlah String tanggal DateTime
createdAt DateTime @default(now()) jenisKelamin String
updatedAt DateTime @updatedAt alamat String
deletedAt DateTime @default(now()) penyakit String
isActive Boolean @default(true) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
// ========================================= ARTIKEL KESEHATAN ========================================= // // ========================================= ARTIKEL KESEHATAN ========================================= //
@@ -1027,16 +1057,17 @@ model DoctorSign {
// ========================================= POSYANDU ========================================= // // ========================================= POSYANDU ========================================= //
model Posyandu { model Posyandu {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
nomor String nomor String
deskripsi String deskripsi String
image FileStorage @relation(fields: [imageId], references: [id]) jadwalPelayanan String
imageId String image FileStorage @relation(fields: [imageId], references: [id])
createdAt DateTime @default(now()) imageId String
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
deletedAt DateTime @default(now()) updatedAt DateTime @updatedAt
isActive Boolean @default(true) deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
// ========================================= PUSKESMAS ========================================= // // ========================================= PUSKESMAS ========================================= //

View File

@@ -125,8 +125,8 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("penghargaan success ..."); console.log("penghargaan success ...");
// =========== LAYANAN DESA =========== // =========== LAYANAN DESA ===========
for (const p of pelayananSuratKeterangan) { for (const p of pelayananSuratKeterangan) {
await prisma.pelayananSuratKeterangan.upsert({ await prisma.pelayananSuratKeterangan.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
@@ -317,63 +317,42 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("visi misi desa success ..."); console.log("visi misi desa success ...");
// Flatten the nested array structure for posisiOrganisasiPPID const flattenedPosisi = posisiOrganisasiPPID.flat();
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisi) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
);
continue;
}
}
for (const p of flattenedPosisiOrganisasiPPID) {
await prisma.posisiOrganisasiPPID.upsert({ await prisma.posisiOrganisasiPPID.upsert({
where: { where: { id: p.id },
id: p.id, update: p,
}, create: p,
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
}); });
} }
console.log("posisi organisasi success ..."); console.log("✅ Posisi organisasi berhasil");
// Flatten the nested array structure for pegawaiPPID // 2. Seed Pegawai
const flattenedPegawaiPPID = pegawaiPPID.flat(); const flattenedPegawai = pegawaiPPID.flat();
for (const p of flattenedPegawai) {
for (const p of flattenedPegawaiPPID) {
await prisma.pegawaiPPID.upsert({ await prisma.pegawaiPPID.upsert({
where: { where: { id: p.id },
id: p.id, update: p,
}, create: p,
update: {
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
create: {
id: p.id,
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
}); });
} }
console.log("pegawai success ..."); console.log("✅ Pegawai berhasil");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({

View File

@@ -5,6 +5,7 @@ import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
//fasilitas kesehatan aja
// Validasi form // Validasi form
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(1, "Nama harus diisi"), name: z.string().min(1, "Nama harus diisi"),
@@ -61,7 +62,7 @@ const defaultForm = {
}, },
}; };
const fasilitasKesehatanState = proxy({ const fasilitasKesehatan = proxy({
create: { create: {
form: { ...defaultForm }, form: { ...defaultForm },
loading: false, loading: false,
@@ -86,7 +87,7 @@ const fasilitasKesehatanState = proxy({
if (res.status === 200) { if (res.status === 200) {
toast.success("Berhasil menambahkan fasilitas kesehatan"); toast.success("Berhasil menambahkan fasilitas kesehatan");
this.resetForm(); this.resetForm();
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
return res.data; return res.data;
} }
} catch (err: any) { } catch (err: any) {
@@ -102,7 +103,6 @@ const fasilitasKesehatanState = proxy({
this.form = { ...defaultForm }; this.form = { ...defaultForm };
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.FasilitasKesehatanGetPayload<{ | Prisma.FasilitasKesehatanGetPayload<{
@@ -156,7 +156,7 @@ const fasilitasKesehatanState = proxy({
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`); const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
fasilitasKesehatanState.findUnique.data = data.data ?? null; fasilitasKesehatan.findUnique.data = data.data ?? null;
} else { } else {
toast.error("Gagal load data fasilitas kesehatan"); toast.error("Gagal load data fasilitas kesehatan");
} }
@@ -176,8 +176,8 @@ const fasilitasKesehatanState = proxy({
const result = await res.json(); const result = await res.json();
const data = result.data; const data = result.data;
fasilitasKesehatanState.edit.id = data.id; fasilitasKesehatan.edit.id = data.id;
fasilitasKesehatanState.edit.form = { fasilitasKesehatan.edit.form = {
name: data.name, name: data.name,
informasiUmum: { informasiUmum: {
fasilitas: data.informasiumum.fasilitas, fasilitas: data.informasiumum.fasilitas,
@@ -205,7 +205,7 @@ const fasilitasKesehatanState = proxy({
}; };
}, },
async submit() { async submit() {
const cek = templateForm.safeParse(fasilitasKesehatanState.edit.form); const cek = templateForm.safeParse(fasilitasKesehatan.edit.form);
if (!cek.success) { if (!cek.success) {
const errMsg = cek.error.issues const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`) .map((v) => `${v.path.join(".")}: ${v.message}`)
@@ -215,42 +215,38 @@ const fasilitasKesehatanState = proxy({
} }
try { try {
fasilitasKesehatanState.edit.loading = true; fasilitasKesehatan.edit.loading = true;
const payload = { const payload = {
name: fasilitasKesehatanState.edit.form.name, name: fasilitasKesehatan.edit.form.name,
informasiUmum: { informasiUmum: {
fasilitas: fasilitas: fasilitasKesehatan.edit.form.informasiUmum.fasilitas,
fasilitasKesehatanState.edit.form.informasiUmum.fasilitas, alamat: fasilitasKesehatan.edit.form.informasiUmum.alamat,
alamat: fasilitasKesehatanState.edit.form.informasiUmum.alamat,
jamOperasional: jamOperasional:
fasilitasKesehatanState.edit.form.informasiUmum.jamOperasional, fasilitasKesehatan.edit.form.informasiUmum.jamOperasional,
}, },
layananUnggulan: { layananUnggulan: {
content: fasilitasKesehatanState.edit.form.layananUnggulan.content, content: fasilitasKesehatan.edit.form.layananUnggulan.content,
}, },
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.name, name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
specialist: specialist:
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.specialist, fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
jadwal: jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.jadwal,
}, },
fasilitasPendukung: { fasilitasPendukung: {
content: content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
fasilitasKesehatanState.edit.form.fasilitasPendukung.content,
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
fasilitasKesehatanState.edit.form.prosedurPendaftaran.content,
}, },
tarifDanLayanan: { tarifDanLayanan: {
layanan: fasilitasKesehatanState.edit.form.tarifDanLayanan.layanan, layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
tarif: fasilitasKesehatanState.edit.form.tarifDanLayanan.tarif, tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
}, },
}; };
const res = await fetch( const res = await fetch(
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatanState.edit.id}`, `/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatan.edit.id}`,
{ {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -264,7 +260,7 @@ const fasilitasKesehatanState = proxy({
} }
toast.success("Berhasil update fasilitas kesehatan"); toast.success("Berhasil update fasilitas kesehatan");
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
return true; return true;
} catch (err) { } catch (err) {
toast.error( toast.error(
@@ -272,37 +268,297 @@ const fasilitasKesehatanState = proxy({
); );
return false; return false;
} finally { } finally {
fasilitasKesehatanState.edit.loading = false; fasilitasKesehatan.edit.loading = false;
} }
}, },
resetForm() { resetForm() {
fasilitasKesehatanState.edit.id = ""; fasilitasKesehatan.edit.id = "";
fasilitasKesehatanState.edit.form = { ...defaultForm }; fasilitasKesehatan.edit.form = { ...defaultForm };
}, },
}, },
delete: { delete: {
loading: false, loading: false,
async byId(id: string){ async byId(id: string) {
try { try {
fasilitasKesehatanState.delete.loading = true; fasilitasKesehatan.delete.loading = true;
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/del/${id}`, { const res = await fetch(
method: "DELETE", `/api/kesehatan/fasilitas-kesehatan/del/${id}`,
}); {
method: "DELETE",
}
);
const result = await res.json(); const result = await res.json();
if (res.ok && result.success) { if (res.ok && result.success) {
toast.success("Fasilitas kesehatan berhasil dihapus"); toast.success("Fasilitas kesehatan berhasil dihapus");
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
} else { } else {
toast.error(result.message || "Gagal menghapus"); toast.error(result.message || "Gagal menghapus");
} }
} catch { } catch {
toast.error("Terjadi kesalahan saat menghapus"); toast.error("Terjadi kesalahan saat menghapus");
} finally { } finally {
fasilitasKesehatanState.delete.loading = false; fasilitasKesehatan.delete.loading = false;
} }
} },
}, },
}); });
//dokter & tenaga medis
const templateDokterForm = z.object({
name: z.string().min(1, "Nama tidak boleh kosong"),
specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
});
const defaultDokterForm = {
name: "",
specialist: "",
jadwal: "",
};
const dokter = proxy({
create: {
create: {
form: defaultDokterForm,
loading: false,
async create() {
const cek = templateDokterForm.safeParse(dokter.create.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
dokter.create.create.loading = true;
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
"create"
].post(dokter.create.create.form);
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Success create");
dokter.create.create.form = { ...defaultDokterForm };
dokter.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
dokter.create.create.loading = false;
}
},
},
},
findMany: {
data: null as
| Prisma.DokterdanTenagaMedisGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
dokter.findMany.loading = true; // ✅ Akses langsung via nama path
dokter.findMany.page = page;
dokter.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
dokter.findMany.data = res.data.data ?? [];
dokter.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dokter.findMany.data = [];
dokter.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch dokter tenaga medis paginated:", err);
dokter.findMany.data = [];
dokter.findMany.totalPages = 1;
} finally {
dokter.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DokterdanTenagaMedisGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`);
if (res.ok) {
const data = await res.json();
dokter.findUnique.data = data.data ?? null;
} else {
console.error(
"Failed to fetch dokter dan tenaga medis",
res.statusText
);
dokter.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching dokter dan tenaga medis", error);
dokter.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultDokterForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/doktertenagamedis/${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,
specialist: data.specialist,
jadwal: data.jadwal,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading dokter dan tenaga medis:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
name: this.form.name,
specialist: this.form.specialist,
jadwal: this.form.jadwal,
};
const cek = templateDokterForm.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dokter.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data dokter dan tenaga medis");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
dokter.delete.loading = true;
const response = await fetch(
`/api/kesehatan/doktertenagamedis/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Dokter dan tenaga medis berhasil dihapus"
);
await dokter.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus dokter dan tenaga medis"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus dokter dan tenaga medis");
} finally {
dokter.delete.loading = false;
}
},
},
});
const fasilitasKesehatanState = proxy({
fasilitasKesehatan,
dokter
});
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -5,20 +6,19 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateGrafikKepuasan = z.object({ const templateGrafikKepuasan = z.object({
label: z.string().min(2, "Label harus diisi"), nama: z.string().min(2, "Nama harus diisi"),
jumlah: z.string().min(1, "Jumlah harus diisi"), tanggal: z.string().min(1, "Tanggal harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
penyakit: z.string().min(1, "Penyakit harus diisi"),
}); });
type GrafikKepuasan = Prisma.GrafikKepuasanGetPayload<{ const defaultForm = {
select: { nama: "",
label: true; tanggal: "",
jumlah: true; jenisKelamin: "",
}; alamat: "",
}>; penyakit: "",
const defaultForm: GrafikKepuasan = {
label: "",
jumlah: ""
}; };
const grafikkepuasan = proxy({ const grafikkepuasan = proxy({
@@ -36,16 +36,15 @@ const grafikkepuasan = proxy({
} }
try { try {
grafikkepuasan.create.loading = true; grafikkepuasan.create.loading = true;
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(grafikkepuasan.create.form); const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(
grafikkepuasan.create.form
);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
grafikkepuasan.create.form = { grafikkepuasan.create.form = { ...defaultForm };
label: "",
jumlah: "",
};
grafikkepuasan.findMany.load(); grafikkepuasan.findMany.load();
return id; return id;
} }
@@ -62,21 +61,49 @@ const grafikkepuasan = proxy({
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.GrafikKepuasanGetPayload<{ omit: { isActive: true } }>[] | Prisma.GrafikKepuasanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.kesehatan.grafikkepuasan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
grafikkepuasan.findMany.data = res.data?.data ?? []; grafikkepuasan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikkepuasan.findMany.page = page;
grafikkepuasan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.grafikkepuasan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikkepuasan.findMany.data = res.data.data ?? [];
grafikkepuasan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikkepuasan.findMany.data = [];
grafikkepuasan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
grafikkepuasan.findMany.data = [];
grafikkepuasan.findMany.totalPages = 1;
} finally {
grafikkepuasan.findMany.loading = false;
} }
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.GrafikKepuasanGetPayload<{ data: null as Prisma.GrafikKepuasanGetPayload<{
omit: { isActive: true } omit: { isActive: true };
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`); const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
@@ -95,88 +122,137 @@ const grafikkepuasan = proxy({
}, },
update: { update: {
id: "", id: "",
form: {...defaultForm}, form: { ...defaultForm },
loading: false, loading: false,
async byId() { async load(id: string) {
}, if (!id) {
async submit() { toast.warn("ID tidak valid");
const id = this.id; return null;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateGrafikKepuasan.safeParse(grafikkepuasan.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
this.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
} }
toast.success("Berhasil update data!"); try {
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// ✅ Optional: refresh list kalau kamu langsung ke halaman list if (!response.ok) {
await grafikkepuasan.findMany.load(); throw new Error(`HTTP error! status: ${response.status}`);
}
return result.data; const result = await response.json();
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik kepuasan");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
grafikkepuasan.delete.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/del/${id}`, { if (result?.success) {
method: "DELETE", const data = result.data;
headers: { this.id = data.id;
"Content-Type": "application/json", this.form = {
}, nama: data.nama,
}); tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
const result = await response.json(); alamat: data.alamat,
penyakit: data.penyakit,
if (response.ok && result?.success) { };
toast.success( return data; // Return the loaded data
result.message || "Grafik kepuasan berhasil dihapus" } else {
); throw new Error(result?.message || "Gagal memuat data");
await grafikkepuasan.findMany.load(); // refresh list }
} else { } catch (error) {
console.error("Error loading grafik kepuasan:", error);
toast.error( toast.error(
result?.message || "Gagal menghapus grafik kepuasan" error instanceof Error ? error.message : "Gagal memuat data"
); );
return null;
} }
} catch (error) { },
console.error("Gagal delete:", error); async submit() {
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan"); const id = this.id;
} finally { if (!id) {
grafikkepuasan.delete.loading = false; toast.warn("ID tidak valid");
} return null;
} }
}
const formData = {
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
penyakit: this.form.penyakit,
};
const cek = templateGrafikKepuasan.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await grafikkepuasan.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data grafik kepuasan");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
grafikkepuasan.delete.loading = true;
const response = await fetch(
`/api/kesehatan/grafikkepuasan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik kepuasan berhasil dihapus");
await grafikkepuasan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik kepuasan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan");
} finally {
grafikkepuasan.delete.loading = false;
}
},
},
}); });
export default grafikkepuasan; export default grafikkepuasan;

View File

@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templatePersentase = z.object({ //persentase kelahiran kematian
const templatePersentaseKelahiran = z.object({
tahun: z.string().min(4, "Tahun harus diisi"), tahun: z.string().min(4, "Tahun harus diisi"),
kematianKasar: z.string().min(1, "Kematian kasar harus diisi"), kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"), kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
@@ -13,18 +16,14 @@ const templatePersentase = z.object({
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{ type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
select: { select: {
tahun: true; kematianId: true;
kematianKasar: true; kelahiranId: true;
kelahiranKasar: true;
kematianBayi: true;
}; };
}>; }>;
const defaultForm: Persentase = { const defaultForm: Persentase = {
tahun: "", kematianId: "",
kematianKasar: "", kelahiranId: "",
kelahiranKasar: "",
kematianBayi: "",
}; };
const persentasekelahiran = proxy({ const persentasekelahiran = proxy({
@@ -32,7 +31,9 @@ const persentasekelahiran = proxy({
form: defaultForm, form: defaultForm,
loading: false, loading: false,
async create() { async create() {
const cek = templatePersentase.safeParse(persentasekelahiran.create.form); const cek = templatePersentaseKelahiran.safeParse(
persentasekelahiran.create.form
);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -47,7 +48,7 @@ const persentasekelahiran = proxy({
].post(persentasekelahiran.create.form); ].post(persentasekelahiran.create.form);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
persentasekelahiran.create.form = { ...defaultForm }; persentasekelahiran.create.form = { ...defaultForm };
@@ -69,21 +70,51 @@ const persentasekelahiran = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.DataKematian_KelahiranGetPayload<{ | Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true }; include: {
kematian: true;
kelahiran: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.kesehatan.persentasekelahiran[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
persentasekelahiran.findMany.data = res.data?.data ?? []; persentasekelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
persentasekelahiran.findMany.page = page;
persentasekelahiran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
persentasekelahiran.findMany.data = res.data.data ?? [];
persentasekelahiran.findMany.totalPages = res.data.totalPages ?? 1;
} else {
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
} finally {
persentasekelahiran.findMany.loading = false;
} }
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.DataKematian_KelahiranGetPayload<{ data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true }; include: {
kematian: true;
kelahiran: true;
};
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
@@ -114,13 +145,11 @@ const persentasekelahiran = proxy({
} }
const formData = { const formData = {
tahun: this.form.tahun, kematianId: this.form.kematianId,
kematianKasar: this.form.kematianKasar, kelahiranId: this.form.kelahiranId,
kelahiranKasar: this.form.kelahiranKasar,
kematianBayi: this.form.kematianBayi,
}; };
const cek = templatePersentase.safeParse(formData); const cek = templatePersentaseKelahiran.safeParse(formData);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -197,4 +226,521 @@ const persentasekelahiran = proxy({
}, },
}); });
export default persentasekelahiran; // data kelahiran
const templateKelahiran = z.object({
nama: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(4, "Tahun harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
});
const defaultKelahiran = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
};
const kelahiran = proxy({
create: {
form: { ...defaultKelahiran }, // ✅ ini kunci fix-nya
loading: false,
async create() {
const cek = templateKelahiran.safeParse(kelahiran.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kelahiran.create.loading = true;
const res = await ApiFetch.api.kesehatan.kelahiran["create"].post(
kelahiran.create.form
);
if (res.status === 200) {
kelahiran.findMany.load();
return toast.success("Kelahiran berhasil disimpan!");
}
return toast.error("Gagal menyimpan kelahiran");
} catch (error) {
console.log((error as Error).message);
} finally {
kelahiran.create.loading = false;
}
},
resetForm() {
kelahiran.create.form = { ...defaultKelahiran };
},
},
findMany: {
data: null as
| Prisma.KelahiranGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
kelahiran.findMany.page = page;
kelahiran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.kelahiran["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kelahiran.findMany.data = res.data.data ?? [];
kelahiran.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kelahiran.findMany.data = [];
kelahiran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kelahiran paginated:", err);
kelahiran.findMany.data = [];
kelahiran.findMany.totalPages = 1;
} finally {
kelahiran.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KelahiranGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kelahiran/${id}`);
if (res.ok) {
const data = await res.json();
kelahiran.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch kelahiran:", res.statusText);
kelahiran.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching kelahiran:", error);
kelahiran.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kelahiran.delete.loading = true;
const response = await fetch(`/api/kesehatan/kelahiran/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kelahiran berhasil dihapus");
await kelahiran.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kelahiran");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kelahiran");
} finally {
kelahiran.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKelahiran },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kelahiran/${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,
tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
alamat: data.alamat,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKelahiran.safeParse(kelahiran.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kelahiran.edit.loading = true;
const response = await fetch(`/api/kesehatan/kelahiran/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
}),
});
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 data kelahiran");
await kelahiran.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kelahiran");
}
} catch (error) {
console.error("Error updating data kelahiran:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kelahiran"
);
return false;
} finally {
kelahiran.edit.loading = false;
}
},
reset() {
kelahiran.edit.id = "";
kelahiran.edit.form = { ...defaultKelahiran };
},
},
});
// data kematian
const templateKematian = z.object({
nama: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(4, "Tahun harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
penyebab: z.string().min(1, "Penyebab harus diisi"),
});
const defaultKematian = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
penyebab: "",
};
const kematian = proxy({
create: {
form: { ...defaultKematian }, // ✅ ini kunci fix-nya
loading: false,
async create() {
const cek = templateKematian.safeParse(kematian.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kematian.create.loading = true;
const res = await ApiFetch.api.kesehatan.kematian["create"].post(
kematian.create.form
);
if (res.status === 200) {
kematian.findMany.load();
return toast.success("Kematian berhasil disimpan!");
}
return toast.error("Gagal menyimpan kematian");
} catch (error) {
console.log((error as Error).message);
} finally {
kematian.create.loading = false;
}
},
resetForm() {
kematian.create.form = { ...defaultKematian };
},
},
findMany: {
data: null as
| Prisma.KematianGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kematian.findMany.loading = true; // ✅ Akses langsung via nama path
kematian.findMany.page = page;
kematian.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.kematian["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kematian.findMany.data = res.data.data ?? [];
kematian.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kematian.findMany.data = [];
kematian.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kematian paginated:", err);
kematian.findMany.data = [];
kematian.findMany.totalPages = 1;
} finally {
kematian.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KematianGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kematian/${id}`);
if (res.ok) {
const data = await res.json();
kematian.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch kematian:", res.statusText);
kematian.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching kematian:", error);
kematian.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kematian.delete.loading = true;
const response = await fetch(`/api/kesehatan/kematian/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kematian berhasil dihapus");
await kematian.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kematian");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kematian");
} finally {
kematian.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKematian },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kematian/${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,
tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
alamat: data.alamat,
penyebab: data.penyebab,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading data kematian:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKematian.safeParse(kematian.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kematian.edit.loading = true;
const response = await fetch(`/api/kesehatan/kematian/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
penyebab: this.form.penyebab,
}),
});
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 data kematian");
await kematian.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kematian");
}
} catch (error) {
console.error("Error updating data kematian:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kematian"
);
return false;
} finally {
kematian.edit.loading = false;
}
},
reset() {
kematian.edit.id = "";
kematian.edit.form = { ...defaultKematian };
},
},
});
const persentaseKelahiranKematian = proxy({
persentasekelahiran,
kelahiran,
kematian
});
export default persentaseKelahiranKematian;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -9,6 +10,7 @@ const templateForm = z.object({
nomor: z.string().min(1, { message: "Nomor is required" }), nomor: z.string().min(1, { message: "Nomor is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }), deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
jadwalPelayanan: z.string().min(1, { message: "Jadwal Pelayanan is required" }),
}); });
const defaultForm = { const defaultForm = {
@@ -16,6 +18,7 @@ const defaultForm = {
nomor: "", nomor: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
jadwalPelayanan: "",
}; };
const posyandustate = proxy({ const posyandustate = proxy({
@@ -50,19 +53,43 @@ const posyandustate = proxy({
} }
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.PosyanduGetPayload<{ | Prisma.PosyanduGetPayload<{
include: { include: {
image: true; image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posyandustate.findMany.loading = true; // ✅ Akses langsung via nama path
posyandustate.findMany.page = page;
posyandustate.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
posyandustate.findMany.data = res.data.data ?? [];
posyandustate.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posyandustate.findMany.data = [];
posyandustate.findMany.totalPages = 1;
} }
}>[] } catch (err) {
| null, console.error("Gagal fetch posyandu paginated:", err);
async load() { posyandustate.findMany.data = [];
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get(); posyandustate.findMany.totalPages = 1;
if (res.status === 200) { } finally {
posyandustate.findMany.data = res.data?.data ?? []; posyandustate.findMany.loading = false;
} }
} },
}, },
findUnique: { findUnique: {
data: null as data: null as
@@ -148,6 +175,7 @@ const posyandustate = proxy({
nomor: data.nomor, nomor: data.nomor,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
imageId: data.imageId || "", imageId: data.imageId || "",
jadwalPelayanan: data.jadwalPelayanan || "",
}; };
return data; return data;
} else { } else {
@@ -181,6 +209,7 @@ const posyandustate = proxy({
nomor: this.form.nomor, nomor: this.form.nomor,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
imageId: this.form.imageId, imageId: this.form.imageId,
jadwalPelayanan: this.form.jadwalPelayanan,
}), }),
}); });

View File

@@ -1,28 +1,28 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { import {
Box, Box,
Button, Button,
Center, Group,
Image, Image,
Paper, Paper,
Select, Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
function EditBerita() { function EditBerita() {
@@ -130,27 +130,62 @@ function EditBerita() {
placeholder="masukkan deskripsi" placeholder="masukkan deskripsi"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor <EditEditor

View File

@@ -3,9 +3,10 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -113,25 +114,62 @@ export default function CreateBerita() {
placeholder="masukkan deskripsi" placeholder="masukkan deskripsi"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
)} </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor <CreateEditor

View File

@@ -47,7 +47,7 @@ function ListBerita({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return <Skeleton h={500} />; return <Skeleton h={500} />;
} }
const filteredData = data || []; const filteredData = data || [];
return ( return (

View File

@@ -4,10 +4,11 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -105,26 +106,62 @@ function EditPenghargaan() {
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara" placeholder="masukkan juara"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import ApiFetch from '@/lib/api-fetch';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; 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 { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import penghargaanState from '../../../_state/desa/penghargaan';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import penghargaanState from '../../../_state/desa/penghargaan';
function CreatePenghargaan() { function CreatePenghargaan() {
@@ -85,25 +86,62 @@ function CreatePenghargaan() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
)} </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -44,21 +45,7 @@ function ProfilePerbekel() {
perbekelState.findUnique.reset(); // opsional: reset juga data lama perbekelState.findUnique.reset(); // opsional: reset juga data lama
}; };
}, [params?.id, router]); }, [params?.id, router]);
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) { if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
@@ -128,27 +115,61 @@ function ProfilePerbekel() {
value={perbekelState.edit.form.biodata} value={perbekelState.edit.form.biodata}
onChange={(val) => perbekelState.edit.form.biodata = val} onChange={(val) => perbekelState.edit.form.biodata = val}
/> />
{/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box> <Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
{previewImage ? ( <Box>
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" /> <Dropzone
) : ( onDrop={(files) => {
<Center w={200} h={200} bg="gray.2"> const selectedFile = files[0]; // Ambil file pertama
<Stack align="center" gap="xs"> if (selectedFile) {
<IconImageInPicture size={48} color="gray" /> setFile(selectedFile);
<Text size="sm" c="gray">Tidak ada gambar</Text> setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
</Stack> }
</Center> }}
)} onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<Box> <Box>

View File

@@ -116,14 +116,14 @@ function ListDetailDataPengangguran() {
{!mounted && !chartData ? ( {!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title> <Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper> </Paper>
</Box> </Box>
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title> <Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
{mounted && chartData.length > 0 && ( {mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}> <Box w={{ base: '100%', md: '70%' }}>
<BarChart <BarChart

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital'; import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -95,27 +96,61 @@ function EditPenghargaan() {
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital'; import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
function CreateDesaDigital() { function CreateDesaDigital() {
@@ -49,7 +50,7 @@ function CreateDesaDigital() {
// Submit the form // Submit the form
const success = await stateDesaDigital.create.create() const success = await stateDesaDigital.create.create()
if (success) { if (success) {
resetForm() resetForm()
router.push("/admin/inovasi/desa-digital-smart-village") router.push("/admin/inovasi/desa-digital-smart-village")
@@ -87,25 +88,61 @@ function CreateDesaDigital() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
)} </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno'; import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -94,27 +95,61 @@ function EditInfoTeknologiTepatGuna() {
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/> />
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg={"gray"}> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno'; import infoTeknoState from '../../../_state/inovasi/info-tekno';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() { function CreateInfoTeknologiTepatGuna() {
@@ -49,7 +50,7 @@ function CreateInfoTeknologiTepatGuna() {
// Submit the form // Submit the form
const success = await stateInfoTekno.create.create() const success = await stateInfoTekno.create.create()
if (success) { if (success) {
resetForm() resetForm()
router.push("/admin/inovasi/info-teknologi-tepat-guna") router.push("/admin/inovasi/info-teknologi-tepat-guna")
@@ -87,25 +88,61 @@ function CreateInfoTeknologiTepatGuna() {
}} }}
/> />
</Box> </Box>
<FileInput <Box>
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
"data:image/png;base64," + Buffer.from(buf).toString("base64") setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> onReject={() => toast.error('File tidak valid.')}
{previewImage ? ( maxSize={5 * 1024 ** 2} // Maks 5MB
<Image alt="" src={previewImage} w={200} h={200} /> accept={{ 'image/*': [] }}
) : ( >
<Center w={200} h={200} bg={"gray"}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconImageInPicture /> <Dropzone.Accept>
</Center> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
)} </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -39,7 +39,7 @@ interface FasilitasKesehatanFormBase {
} }
function EditFasilitasKesehatan() { function EditFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState); const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -12,7 +12,7 @@ import { useProxy } from 'valtio/utils';
function DetailFasilitasKesehatan() { function DetailFasilitasKesehatan() {
const params = useParams() const params = useParams()
const router = useRouter(); const router = useRouter();
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -45,9 +45,23 @@ function DetailFasilitasKesehatan() {
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
</Box> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Stack> <Stack>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text> <Grid>
<GridCol span={12}>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text>
</GridCol>
<GridCol span={12}>
<Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}>
Tambah Dokter
</Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/layanan-unggulan/create`)}>
Tambah Layanan
</Button>
</Flex>
</GridCol>
</Grid>
{stateFasilitasKesehatan.findUnique.data ? ( {stateFasilitasKesehatan.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}> <Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
@@ -68,6 +82,14 @@ function DetailFasilitasKesehatan() {
<Text fz={"md"} fw={"bold"}>Layanan Unggulan</Text> <Text fz={"md"} fw={"bold"}>Layanan Unggulan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.layananunggulan.content }} /> <Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.layananunggulan.content }} />
</Box> </Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Fasilitas Pendukung</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.fasilitaspendukung.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Prosedur Pendaftaran</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.prosedurpendaftaran.content }} />
</Box>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Dokter dan Tenaga Medis</Text> <Text fz={"md"} fw={"bold"}>Dokter dan Tenaga Medis</Text>
<Text fz={"md"} fw={"bold"}>Nama Dokter</Text> <Text fz={"md"} fw={"bold"}>Nama Dokter</Text>
@@ -77,14 +99,6 @@ function DetailFasilitasKesehatan() {
<Text fz={"md"} fw={"bold"}>Jadwal</Text> <Text fz={"md"} fw={"bold"}>Jadwal</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.jadwal}</Text> <Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.jadwal}</Text>
</Box> </Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Fasilitas Pendukung</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.fasilitaspendukung.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Prosedur Pendaftaran</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.prosedurpendaftaran.content }} />
</Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Tarif dan Layanan</Text> <Text fz={"lg"} fw={"bold"}>Tarif dan Layanan</Text>
<Text fz={"md"} fw={"bold"}>Layanan</Text> <Text fz={"md"} fw={"bold"}>Layanan</Text>
@@ -111,6 +125,7 @@ function DetailFasilitasKesehatan() {
</Paper> </Paper>
) : null} ) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Hapus */}

View File

@@ -10,7 +10,7 @@ import { useProxy } from 'valtio/utils';
function CreateFasilitasKesehatan() { function CreateFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
const router = useRouter(); const router = useRouter();

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateDokter() {
const params = useParams()
const createState = useProxy(fasilitasKesehatanState.dokter)
const router = useRouter();
const resetForm = () => {
createState.create.create.form = {
name: "",
specialist: "",
jadwal: "",
};
};
const handleSubmit = async () => {
await createState.create.create.create();
resetForm();
router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`)
};
return (
<Box component="form" onSubmit={handleSubmit}>
<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 Dokter</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
placeholder="masukkan nama dokter"
value={createState.create.create.form.name}
onChange={(e) => {
createState.create.create.form.name = e.target.value;
}}
/>
<Text fz="md" fw="bold">Specialist</Text>
<TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>}
placeholder="masukkan specialist"
value={createState.create.create.form.specialist}
onChange={(e) => {
createState.create.create.form.specialist = e.target.value;
}}
/>
<Box>
<Text fz="md" fw="bold">Jadwal</Text>
<CreateEditor
value={createState.create.create.form.jadwal}
onChange={(htmlContent) => {
createState.create.create.form.jadwal = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateDokter;

View File

@@ -0,0 +1,112 @@
'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 { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import { useState } from 'react';
function DokterTenagaMedis() {
const [search, setSearch] = useState("");
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<HeaderSearch
title='Dokter dan Tenaga Medis'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDokterTenagaMedis search={search} />
</Box>
);
}
function ListDokterTenagaMedis({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter)
const router = useRouter();
const {
data,
loading,
load,
page,
totalPages
} = stateFasilitasKesehatan.findMany
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Fasilitas Kesehatan'
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`}
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Fasilitas Kesehatan</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Jam Operasional</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.specialist}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
}
export default DokterTenagaMedis;

View File

@@ -1,8 +1,8 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Flex, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconList, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
@@ -13,22 +13,37 @@ import { useState } from 'react';
function FasilitasKesehatan() { function FasilitasKesehatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter();
return ( return (
<Box> <Box>
<HeaderSearch <Grid>
title='Fasilitas Kesehatan' <GridCol span={12}>
placeholder='pencarian' <HeaderSearch
searchIcon={<IconSearch size={20} />} title='Fasilitas Kesehatan'
value={search} placeholder='pencarian'
onChange={(e) => setSearch(e.currentTarget.value)} searchIcon={<IconSearch size={20} />}
/> value={search}
<ListFasilitasKesehatan search={search}/> onChange={(e) => setSearch(e.currentTarget.value)}
/>
</GridCol>
<GridCol span={12}>
<Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`)}>
<IconList size={20} /> List Dokter
</Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan`)}>
<IconList size={20} /> List Layanan
</Button>
</Flex>
</GridCol>
</Grid>
<ListFasilitasKesehatan search={search} />
</Box> </Box>
); );
} }
function ListFasilitasKesehatan({ search }: { search: string }) { function ListFasilitasKesehatan({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
@@ -47,47 +62,47 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
if (!stateFasilitasKesehatan.findMany.data) { if (!stateFasilitasKesehatan.findMany.data) {
return ( return (
<Box py={10}> <Box py={10}>
<Skeleton h={500}/> <Skeleton h={500} />
</Box> </Box>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Stack> <Stack>
<JudulList <JudulList
title='List Fasilitas Kesehatan' title='List Fasilitas Kesehatan'
href='/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create' href='/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create'
/> />
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Fasilitas Kesehatan</TableTh> <TableTh>Fasilitas Kesehatan</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Dokter</TableTh>
<TableTh>Jam Operasional</TableTh> <TableTh>Layanan</TableTh>
<TableTh>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.informasiumum.alamat}</TableTd>
<TableTd>{item.informasiumum.jamOperasional}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.map((item) => (
</Box> <TableTr key={item.id}>
</Stack> <TableTd>{item.name}</TableTd>
</Paper> <TableTd>{item.dokterdantenagamedis.name}</TableTd>
</Box> <TableTd>{item.tarifdanlayanan.layanan}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
) )
} }

View File

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

View File

@@ -0,0 +1,116 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() {
const editState = useProxy(grafikkepuasan)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.update.form.nama || '',
tanggal: editState.update.form.tanggal || '',
jenisKelamin: editState.update.form.jenisKelamin || '',
alamat: editState.update.form.alamat || '',
penyakit: editState.update.form.penyakit || '',
});
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyakit: data.penyakit || '',
});
}
} catch (error) {
console.error("Error loading grafik hasil kepuasan:", error);
toast.error("Gagal memuat data grafik hasil kepuasan");
}
};
loadKelahiran();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
penyakit: formData.penyakit,
};
await editState.update.submit();
toast.success('grafik hasil kepuasan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
} catch (error) {
console.error('Error updating grafik hasil kepuasan:', error);
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
}
};
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 grafik hasil kepuasan</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
type='date'
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>}
placeholder="masukkan tanggal"
/>
<TextInput
value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>}
placeholder="masukkan jenis kelamin"
/>
<TextInput
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
placeholder="masukkan alamat"
/>
<TextInput
value={formData.penyakit}
onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Penyakit</Text>}
placeholder="masukkan penyakit"
/>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikHasilKepuasan;

View File

@@ -1,80 +1,125 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() { import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
function DetailGrafikHasilKepuasan() {
const state = useProxy(grafikkepuasan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter() const router = useRouter()
const params = useParams() as { id: string }
const stateGrafikKepuasan = useProxy(grafikkepuasan)
const id = params.id useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
// Load data saat komponen mount
useEffect(() => { const handleHapus = () => {
if (id) { if (selectedId) {
stateGrafikKepuasan.findUnique.load(id).then(() => { state.delete.byId(selectedId)
const data = stateGrafikKepuasan.findUnique.data setModalHapus(false)
if (data) { setSelectedId(null)
stateGrafikKepuasan.update.form = { router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan")
label: data.label || '',
jumlah: data.jumlah || '',
}
}
})
} }
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateGrafikKepuasan.update.id = id;
await stateGrafikKepuasan.update.submit();
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan')
} }
return (
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box> <Box>
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
</Box> </Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> <Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack> <Stack>
<Title order={3}>Edit Grafik Hasil Kepuasan</Title> <Text fz={"xl"} fw={"bold"}>Detail Data Grafik Hasil Kepuasan</Text>
<TextInput {state.findUnique.data ? (
label="Label" <Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
placeholder="masukkan label" <Stack gap={"xs"}>
value={stateGrafikKepuasan.update.form.label} <Box>
onChange={(val) => { <Text fw={"bold"} fz={"lg"}>Nama</Text>
stateGrafikKepuasan.update.form.label = val.currentTarget.value; <Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
}} </Box>
/> <Box>
<TextInput <Text fw={"bold"} fz={"lg"}>Tanggal</Text>
label="Jumlah" <Text fz={"lg"}>
type="number" {new Date(state.findUnique.data?.tanggal).toLocaleDateString('id-ID', {
placeholder="masukkan jumlah" day: '2-digit',
value={stateGrafikKepuasan.update.form.jumlah} month: 'long',
onChange={(val) => { year: 'numeric'
stateGrafikKepuasan.update.form.jumlah = val.currentTarget.value; })}
}} </Text>
/> </Box>
<Button <Box>
mt={10} <Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
bg={colors['blue-button']} <Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text>
onClick={handleSubmit} </Box>
> <Box>
Simpan <Text fw={"bold"} fz={"lg"}>Alamat</Text>
</Button> <Text fz={"lg"} >{state.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Penyakit</Text>
<Text fz={"lg"} >{state.findUnique.data?.penyakit}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus data ini?'
/>
</Box> </Box>
) );
} }
export default EditGrafikHasilKepuasan; export default DetailGrafikHasilKepuasan;

View File

@@ -16,20 +16,16 @@ function CreateGrafikHasilKepuasanMasyarakat() {
const resetForm = () => { const resetForm = () => {
stateGrafikKepuasan.create.form = { stateGrafikKepuasan.create.form = {
label: "", nama: "",
jumlah: "", tanggal: "",
jenisKelamin: "",
alamat: "",
penyakit: "",
} }
} }
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateGrafikKepuasan.create.create(); await stateGrafikKepuasan.create.create();
if (id) {
const idStr = String(id);
await stateGrafikKepuasan.findUnique.load(idStr);
if (stateGrafikKepuasan.findUnique.data) {
setChartData([stateGrafikKepuasan.findUnique.data]);
}
}
resetForm(); resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
} }
@@ -45,21 +41,48 @@ function CreateGrafikHasilKepuasanMasyarakat() {
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> <Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<TextInput <TextInput
label="Label" label="Nama"
type="text" type="text"
value={stateGrafikKepuasan.create.form.label} value={stateGrafikKepuasan.create.form.nama}
placeholder="Masukkan label" placeholder="Masukkan nama"
onChange={(val) => { onChange={(val) => {
stateGrafikKepuasan.create.form.label = val.currentTarget.value; stateGrafikKepuasan.create.form.nama = val.currentTarget.value;
}} }}
/> />
<TextInput <TextInput
label="Jumlah" label="Tanggal"
type="number" type="date"
value={stateGrafikKepuasan.create.form.jumlah} value={stateGrafikKepuasan.create.form.tanggal}
placeholder="Masukkan jumlah" placeholder="Masukkan tanggal"
onChange={(val) => { onChange={(val) => {
stateGrafikKepuasan.create.form.jumlah = val.currentTarget.value; stateGrafikKepuasan.create.form.tanggal = val.currentTarget.value;
}}
/>
<TextInput
label="Jenis Kelamin"
type="text"
value={stateGrafikKepuasan.create.form.jenisKelamin}
placeholder="Masukkan jenis kelamin"
onChange={(val) => {
stateGrafikKepuasan.create.form.jenisKelamin = val.currentTarget.value;
}}
/>
<TextInput
label="Alamat"
type="text"
value={stateGrafikKepuasan.create.form.alamat}
placeholder="Masukkan alamat"
onChange={(val) => {
stateGrafikKepuasan.create.form.alamat = val.currentTarget.value;
}}
/>
<TextInput
label="Penyakit"
type="text"
value={stateGrafikKepuasan.create.form.penyakit}
placeholder="Masukkan penyakit"
onChange={(val) => {
stateGrafikKepuasan.create.form.penyakit = val.currentTarget.value;
}} }}
/> />
<Group> <Group>

View File

@@ -1,16 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; 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, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
function GrafikHasilKepuasanMasyarakat() { function GrafikHasilKepuasanMasyarakat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -30,53 +30,79 @@ function GrafikHasilKepuasanMasyarakat() {
function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
type PDKMGrafik = { type PDKMGrafik = {
id: string; id: string;
label: string; nama: string;
jumlah: number; tanggal: string | Date; // Allow both string and Date types
jenisKelamin: string;
alamat: string;
penyakit: string;
createdAt?: Date; // Add optional fields that might come from the API
updatedAt?: Date;
deletedAt?: Date | null;
} }
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [chartData, setChartData] = useState<PDKMGrafik[]>([]); const [chartData, setChartData] = useState<PDKMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)') const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const handleDelete = () => { const {
if (selectedId) { data,
stateGrafikKepuasan.delete.byId(selectedId) page,
setModalHapus(false) totalPages,
setSelectedId(null) loading,
load
stateGrafikKepuasan.findMany.load() } = stateGrafikKepuasan.findMany;
}
}
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true)
stateGrafikKepuasan.findMany.load() load(page, 10, search)
}, []) }, [page, search])
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
if (stateGrafikKepuasan.findMany.data) { if (data) {
setChartData(stateGrafikKepuasan.findMany.data.map((item) => ({ setChartData(data.map((item) => ({
id: item.id, id: item.id,
label: item.label, nama: item.nama,
jumlah: Number(item.jumlah), tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal,
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
}))); })));
} }
}, [stateGrafikKepuasan.findMany.data]); }, [data]);
const filteredData = (stateGrafikKepuasan.findMany.data || []).filter(item => { // Add this function to process the data
const keyword = search.toLowerCase(); const processDiseaseData = (data: PDKMGrafik[]) => {
return ( const diseaseCount: Record<string, number> = {};
item.label.toLowerCase().includes(keyword) ||
item.jumlah.toString().toLowerCase().includes(keyword)
);
});
if (!stateGrafikKepuasan.findMany.data) { data.forEach(item => {
const penyakit = item.penyakit.trim();
if (penyakit) {
diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
}
});
return Object.entries(diseaseCount).map(([name, count]) => ({
name,
count
}));
};
// Add this state to store the processed chart data
const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]);
// Update the chart data when data changes
useEffect(() => {
if (data && data.length > 0) {
setDiseaseChartData(processDiseaseData(data));
}
}, [data]);
const filteredData = data || [];
if (loading || !data) {
return ( return (
<Stack> <Stack>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -89,40 +115,36 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<Stack gap={"xs"}> <Stack gap={"xs"}>
{/* Form Input */} {/* Form Input */}
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulListTab <JudulList
title='List Grafik Hasil Kepuasan Masyarakat' title='List Grafik Hasil Kepuasan Masyarakat'
href='/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create' href='/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Label</TableTh> <TableTh>Nama</TableTh>
<TableTh>Jumlah</TableTh> <TableTh>Tanggal</TableTh>
<TableTh>Edit</TableTh> <TableTh>Jenis Kelamin</TableTh>
<TableTh>Delete</TableTh> <TableTh>Penyakit</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.label}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.jumlah}</TableTd> <TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.penyakit}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`)}> <Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`)}>
<IconEdit size={20} /> <IconDeviceImacCog size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateGrafikKepuasan.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -130,6 +152,15 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */} {/* Chart */}
@@ -141,30 +172,29 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</Paper> </Paper>
</Box> </Box>
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && chartData.length > 0 && ( {mounted && diseaseChartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} > <BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} >
<XAxis dataKey="label" /> <XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Legend /> <Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah" /> <Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart> </BarChart>
)} )}
</Paper> </Paper>
</Box> </Box>
)} )}
</Stack> </Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik hasil kepuasan masyarakat ini?'
/>
</Box> </Box>
); );
} }

View File

@@ -1,113 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPersentaseDataKelahiranKematian() {
const router = useRouter()
const params = useParams() as { id: string }
const statePresentase = useProxy(persentasekelahiran)
const id = params.id
// Load data saat komponen mount
// Di file page.tsx, ubah useEffect-nya menjadi:
useEffect(() => {
if (!id) return;
statePresentase.update.id = id;
statePresentase.findUnique.load(id)
.then(() => {
const data = statePresentase.findUnique.data;
if (data) {
statePresentase.update.form = {
tahun: String(data.tahun || ''),
kematianKasar: String(data.kematianKasar || ''),
kelahiranKasar: String(data.kelahiranKasar || ''),
kematianBayi: String(data.kematianBayi || '')
};
}
})
.catch(error => {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => {
try {
statePresentase.update.id = id;
await statePresentase.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Persentase Data Kelahiran & Kematian</Title>
<TextInput
label="Tahun"
placeholder="masukkan tahun"
value={statePresentase.update.form.tahun}
onChange={(val) => {
statePresentase.update.form.tahun = val.currentTarget.value;
}}
/>
<TextInput
label="Kematian Kasar"
type="number"
placeholder="masukkan kematian kasar"
value={statePresentase.update.form.kematianKasar}
onChange={(val) => {
statePresentase.update.form.kematianKasar = val.currentTarget.value;
}}
/>
<TextInput
label="Kematian Bayi"
type="number"
placeholder="masukkan kematian bayi"
value={statePresentase.update.form.kematianBayi}
onChange={(val) => {
statePresentase.update.form.kematianBayi = val.currentTarget.value;
}}
/>
<TextInput
label="Kelahiran Kasar"
type="number"
placeholder="masukkan kelahiran kasar"
value={statePresentase.update.form.kelahiranKasar}
onChange={(val) => {
statePresentase.update.form.kelahiranKasar = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan Perubahan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditPersentaseDataKelahiranKematian;

View File

@@ -1,101 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function CreatePersentaseDataKelahiranKematian() {
const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
statePersentase.create.form = {
tahun: "",
kematianBayi: "",
kematianKasar: "",
kelahiranKasar: "",
}
}
const handleSubmit = async () => {
const id = await statePersentase.create.create();
if (id) {
const idStr = String(id);
await statePersentase.findUnique.load(idStr);
if (statePersentase.findUnique.data) {
setChartData([statePersentase.findUnique.data]);
}
}
resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian");
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Persentase Data Kelahiran & Kematian</Title>
<Stack gap={"xs"}>
<TextInput
label="Tahun"
type="number"
value={statePersentase.create.form.tahun}
placeholder="Masukkan tahun"
onChange={(val) => {
statePersentase.create.form.tahun = val.currentTarget.value;
}}
/>
<TextInput
label="Kematian Kasar"
type="number"
value={statePersentase.create.form.kematianKasar}
placeholder="Masukkan kematian kasar"
onChange={(val) => {
statePersentase.create.form.kematianKasar = val.currentTarget.value;
}}
/>
<TextInput
label="Kematian Bayi"
type="number"
value={statePersentase.create.form.kematianBayi}
placeholder="Masukkan kematian bayi"
onChange={(val) => {
statePersentase.create.form.kematianBayi = val.currentTarget.value;
}}
/>
<TextInput
label="Kelahiran Kasar"
type="number"
value={statePersentase.create.form.kelahiranKasar}
placeholder="Masukkan kelahiran kasar"
onChange={(val) => {
statePersentase.create.form.kelahiranKasar = val.currentTarget.value;
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreatePersentaseDataKelahiranKematian;

View File

@@ -0,0 +1,107 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKelahiran() {
const editState = useProxy(persentaseKelahiranKematian.kelahiran)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
});
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
});
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error("Gagal memuat data data kelahiran");
}
};
loadKelahiran();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.edit.form = {
...editState.edit.form,
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
};
await editState.edit.update();
toast.success('data kelahiran berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
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 data kelahiran</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
type='date'
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>}
placeholder="masukkan tanggal"
/>
<TextInput
value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>}
placeholder="masukkan jenis kelamin"
/>
<TextInput
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
placeholder="masukkan alamat"
/>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKelahiran;

View File

@@ -0,0 +1,121 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
function DetailKelahiran() {
const state = useProxy(persentaseKelahiranKematian.kelahiran)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Kelahiran</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"}>Tanggal</Text>
<Text fz={"lg"}>
{new Date(state.findUnique.data?.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus data ini?'
/>
</Box>
);
}
export default DetailKelahiran;

View File

@@ -0,0 +1,83 @@
'use client'
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKelahiran() {
const createState = useProxy(persentaseKelahiranKematian.kelahiran)
const router = useRouter();
const resetForm = () => {
createState.create.form = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
};
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kelahiran</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
placeholder='Masukkan nama'
value={createState.create.form.nama}
onChange={(val) => {
createState.create.form.nama = val.target.value;
}}
/>
<TextInput
type='date'
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>}
placeholder='Masukkan tanggal'
value={createState.create.form.tanggal}
onChange={(val) => {
createState.create.form.tanggal = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jenis Kelamin</Text>}
placeholder='Masukkan jenis kelamin'
value={createState.create.form.jenisKelamin}
onChange={(val) => {
createState.create.form.jenisKelamin = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat</Text>}
placeholder='Masukkan alamat'
value={createState.create.form.alamat}
onChange={(val) => {
createState.create.form.alamat = val.target.value;
}}
/>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKelahiran;

View File

@@ -0,0 +1,118 @@
'use client'
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function Kelahiran() {
const router = useRouter();
const [search, setSearch] = useState("");
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<HeaderSearch
title='Data Kelahiran'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKelahiran search={search} />
</Box>
);
}
function ListKelahiran({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kelahiran);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load
} = statePersentase.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Stack gap={"xs"}>
{/* Form Input */}
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Data Kelahiran'
href='/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`)}>
<IconDeviceImacCog size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Stack>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default Kelahiran;

View File

@@ -0,0 +1,121 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKematian() {
const editState = useProxy(persentaseKelahiranKematian.kematian)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
penyebab: editState.edit.form.penyebab || '',
});
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyebab: data.penyebab || '',
});
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error("Gagal memuat data data kelahiran");
}
};
loadKelahiran();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.edit.form = {
...editState.edit.form,
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
penyebab: formData.penyebab,
};
await editState.edit.update();
toast.success('data kelahiran berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
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 data kelahiran</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
type='date'
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>}
placeholder="masukkan tanggal"
/>
<TextInput
value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>}
placeholder="masukkan jenis kelamin"
/>
<TextInput
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
placeholder="masukkan alamat"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Penyebab</Text>
<EditEditor
value={formData.penyebab}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, penyebab: htmlContent }));
editState.edit.form.penyebab = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKematian;

View File

@@ -0,0 +1,126 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
function DetailKematian() {
const state = useProxy(persentaseKelahiranKematian.kematian)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Kematian</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"}>Tanggal</Text>
<Text fz={"lg"}>
{state.findUnique.data?.tanggal instanceof Date
? state.findUnique.data.tanggal.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})
: state.findUnique.data?.tanggal}
</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Penyebab</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.penyebab || '' }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus data ini?'
/>
</Box>
);
}
export default DetailKematian;

View File

@@ -0,0 +1,94 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKematian() {
const createState = useProxy(persentaseKelahiranKematian.kematian)
const router = useRouter();
const resetForm = () => {
createState.create.form = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
penyebab: "",
};
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kematian</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
placeholder='Masukkan nama'
value={createState.create.form.nama}
onChange={(val) => {
createState.create.form.nama = val.target.value;
}}
/>
<TextInput
type='date'
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>}
placeholder='Masukkan tanggal'
value={createState.create.form.tanggal}
onChange={(val) => {
createState.create.form.tanggal = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jenis Kelamin</Text>}
placeholder='Masukkan jenis kelamin'
value={createState.create.form.jenisKelamin}
onChange={(val) => {
createState.create.form.jenisKelamin = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat</Text>}
placeholder='Masukkan alamat'
value={createState.create.form.alamat}
onChange={(val) => {
createState.create.form.alamat = val.target.value;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Penyebab</Text>
<CreateEditor
value={createState.create.form.penyebab}
onChange={(htmlContent) => {
createState.create.form.penyebab = htmlContent;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKematian;

View File

@@ -0,0 +1,118 @@
'use client'
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function Kematian() {
const [search, setSearch] = useState("");
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<HeaderSearch
title='Data Kematian'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKematian search={search} />
</Box >
);
}
function ListKematian({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kematian);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load
} = statePersentase.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [search])
const filteredData = data || []
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Stack gap={"xs"}>
{/* Form Input */}
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Data Kematian'
href='/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Stack>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default Kematian;

View File

@@ -1,183 +1,227 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { ActionIcon, Box, Center, Flex, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Legend, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; type TooltipPayload = {
import HeaderSearch from '../../../_com/header'; name: string;
value: number;
payload: any;
color: string;
dataKey: string;
};
type CustomTooltipProps = TooltipProps<number, string> & {
active?: boolean;
payload?: TooltipPayload[];
label?: string;
};
function PersentaseDataKelahiranKematian() { function PersentaseDataKelahiranKematian() {
const [search, setSearch] = useState("");
return ( return (
<Box> <Stack gap={"xs"}>
<HeaderSearch <GrafikPersentaseKelahiranKematian />
title='Persentase Data Kelahiran & Kematian' </Stack>
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPersentaseDataKelahiranKematian search={search} />
</Box>
); );
} }
function ListPersentaseDataKelahiranKematian({ search }: { search: string }) { function GrafikPersentaseKelahiranKematian() {
type PDKMGrafik = {
id: string;
tahun: string;
kematianKasar: number;
kematianBayi: number;
kelahiranKasar: number;
}
const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<PDKMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
type DataTahunan = {
tahun: string;
totalKelahiran: number;
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
const handleDelete = () => { // Count occurrences per year
if (selectedId) { const countByYear = (data: any[], dateField: string) => {
statePersentase.delete.byId(selectedId) const counts: Record<string, number> = {};
setModalHapus(false) data?.forEach(item => {
setSelectedId(null) const year = new Date(item[dateField]).getFullYear().toString();
counts[year] = (counts[year] || 0) + 1;
});
return counts;
};
statePersentase.findMany.load() const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<DataTahunan[]>([]);
const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)');
const [selectedYear, setSelectedYear] = useState<string | null>(null);
// Format number to Indonesian locale
const formatNumber = (num: number) => {
return new Intl.NumberFormat('id-ID').format(num);
};
// Format tooltip
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<Paper p="md" shadow="md" withBorder>
<Text size="sm" fw={500} mb={5}>Tahun {label}</Text>
<Text size="sm" c="blue">Kelahiran: {formatNumber(payload[0].value)}</Text>
<Text size="sm" c="red">Kematian: {formatNumber(payload[1].value)}</Text>
</Paper>
);
} }
} return null;
};
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) statePersentase.kelahiran.findMany.load(1, 1000); // Load all kelahiran data
statePersentase.findMany.load() statePersentase.kematian.findMany.load(1, 1000); // Load all kematian data
}, []) }, []);
useEffect(() => { useEffect(() => {
setMounted(true); if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
if (statePersentase.findMany.data) { // Count kelahiran and kematian by year
setChartData(statePersentase.findMany.data.map((item) => ({ const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal');
id: item.id, const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal');
tahun: item.tahun,
kematianKasar: Number(item.kematianKasar), // Get all unique years
kematianBayi: Number(item.kematianBayi), const allYears = new Set([
kelahiranKasar: Number(item.kelahiranKasar), ...Object.keys(kelahiranByYear),
}))); ...Object.keys(kematianByYear)
]);
// Create data structure for the chart
const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => {
acc[year] = {
tahun: year,
totalKelahiran: kelahiranByYear[year] || 0,
totalKematian: kematianByYear[year] || 0,
data: []
};
return acc;
}, {});
const sortedData = Object.values(dataByYear).sort((a, b) =>
parseInt(a.tahun) - parseInt(b.tahun)
);
setChartData(sortedData);
setSelectedYear(sortedData[0]?.tahun || '');
} }
}, [statePersentase.findMany.data]); }, [
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data,
]);
const filteredData = (statePersentase.findMany.data || []).filter(item => { if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
const keyword = search.toLowerCase();
return (
item.tahun.toLowerCase().includes(keyword) ||
item.kematianKasar.toString().toLowerCase().includes(keyword) ||
item.kematianBayi.toString().toLowerCase().includes(keyword) ||
item.kelahiranKasar.toString().toLowerCase().includes(keyword)
);
});
if (!statePersentase.findMany.data) {
return ( return (
<Stack> <Stack>
<Skeleton h={500} /> <Skeleton h={500} />
</Stack> </Stack>
) );
} }
const selectedYearData = chartData.find(d => d.tahun === selectedYear);
return ( return (
<Box> <Paper bg={colors['white-1']} p="md">
<Stack gap={"xs"}> <Stack gap={"xs"}>
{/* Form Input */} <Title order={3} mb="md">Statistik Kelahiran & Kematian</Title>
<Paper bg={colors['white-1']} p={'md'}> <Box>
<JudulListTab <Flex gap={"xs"}>
title='List Persentase Data Kelahiran & Kematian' <Box>
href='/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/create' <ActionIcon size={30} color={colors['blue-button']} onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
placeholder='pencarian' <IconBabyCarriage size={30} color={colors['white-1']} />
searchIcon={<IconSearch size={16} />} </ActionIcon>
/> </Box>
<Table striped withTableBorder withRowBorders> <Box>
<TableThead> <ActionIcon size={30} color={colors['blue-button']} onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')} >
<TableTr> <IconGrave2 size={30} color={colors['white-1']} />
<TableTh>Tahun</TableTh> </ActionIcon>
<TableTh>Kematian Kasar</TableTh> </Box>
<TableTh>Kematian Bayi</TableTh> </Flex>
<TableTh>kelahiran Kasar</TableTh> </Box>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd>{item.kematianKasar}</TableTd>
<TableTd>{item.kematianBayi}</TableTd>
<TableTd>{item.kelahiranKasar}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={statePersentase.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="kematianKasar" fill="#f03e3e" name="Kematian Kasar" />
<Bar dataKey="kematianBayi" fill="#ff922b" name="Kematian Bayi" />
<Bar dataKey="kelahiranKasar" fill="#4dabf7" name="Kelahiran Kasar" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack> </Stack>
{/* Modal Konfirmasi Hapus */} {chartData.length === 0 ? (
<ModalKonfirmasiHapus <Text c="dimmed" ta="center" py="xl">
opened={modalHapus} Belum ada data yang tersedia untuk ditampilkan
onClose={() => setModalHapus(false)} </Text>
onConfirm={handleDelete} ) : (
text='Apakah anda yakin ingin menghapus persentase data kelahiran & kematian ini?' <>
/> {/* Year Selector */}
</Box> <Box mb="md" style={{ maxWidth: '200px' }}>
<Select
label="Pilih Tahun"
placeholder="Pilih Tahun"
data={chartData.map((item) => ({
value: item.tahun,
label: item.tahun
}))}
value={selectedYear}
onChange={(value) => setSelectedYear(value || '')}
size="xs"
/>
</Box>
{/* Main Chart */}
<Center>
<Box h={400}>
<BarChart
width={isMobile ? window.innerWidth * 0.9 : isTablet ? 700 : 800}
height={350}
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="totalKelahiran" name="Total Kelahiran" fill="#4dabf7" />
<Bar dataKey="totalKematian" name="Total Kematian" fill="#f03e3e" />
</BarChart>
</Box>
</Center>
{/* Yearly Breakdown */}
{selectedYearData && (
<Box mt="xl">
<Title order={4} mb="md">Rincian Tahun {selectedYear}</Title>
<Table striped withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Bulan</Table.Th>
<Table.Th ta="right">Kelahiran</Table.Th>
<Table.Th ta="right">Kematian</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{selectedYearData.data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.bulan}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kematian)}</Table.Td>
</Table.Tr>
))}
<Table.Tr style={{ fontWeight: 'bold' }}>
<Table.Td>Total</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKematian)}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Box>
)}
</>
)}
</Paper>
); );
} }

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -91,57 +92,91 @@ function EditInfoWabahPenyakit() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Info Wabah Penyakit</Title> <Title order={3}>Edit Info Wabah Penyakit</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box>
<FileInput <TextInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} value={formData.deskripsiSingkat}
value={file} onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
onChange={async (e) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
if (!e) return; placeholder="masukkan deskripsi"
setFile(e); />
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz="sm" fw="bold">Deskripsi</Text>
) : ( <EditEditor
<Center w={200} h={200} bg="gray"> value={formData.deskripsi}
<IconImageInPicture /> onChange={(val) => setFormData({ ...formData, deskripsi: val })}
</Center> />
)} </Box>
<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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <div>
Simpan <Text size="xl" inline>
</Button> Drag gambar ke sini atau klik untuk pilih file
</Stack> </Text>
</Paper> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoWabahPenyakit() { function CreateInfoWabahPenyakit() {
const router = useRouter(); const router = useRouter();
@@ -94,28 +95,62 @@ function CreateInfoWabahPenyakit() {
}} }}
/> />
</Box> </Box>
<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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -89,49 +90,82 @@ function EditKontakDarurat() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Kontak Darurat</Title> <Title order={3}>Edit Kontak Darurat</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
<Button onClick={handleSubmit} bg={colors['blue-button']}> maxHeight: '200px',
Simpan objectFit: 'contain',
</Button> borderRadius: '8px',
</Stack> border: '1px solid #ddd',
</Paper> }}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,113 +1,147 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import ApiFetch from '@/lib/api-fetch';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { Dropzone } from '@mantine/dropzone';
function CreateKontakDarurat() { function CreateKontakDarurat() {
const router = useRouter(); const router = useRouter();
const kontakDaruratState = useProxy(kontakDarurat) const kontakDaruratState = useProxy(kontakDarurat)
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
kontakDaruratState.create.form = { kontakDaruratState.create.form = {
name: "", name: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
};
setPreviewImage(null);
setFile(null);
}; };
setPreviewImage(null);
const handleSubmit = async () => { setFile(null);
if (!file) { };
return toast.warn("Pilih file gambar terlebih dahulu");
} const handleSubmit = async () => {
if (!file) {
const res = await ApiFetch.api.fileStorage.create.post({ return toast.warn("Pilih file gambar terlebih dahulu");
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakDaruratState.create.form.imageId = uploaded.id;
await kontakDaruratState.create.create();
resetForm();
router.push("/admin/kesehatan/kontak-darurat")
} }
return (
<Box> const res = await ApiFetch.api.fileStorage.create.post({
<Box mb={10}> file,
<Button variant="subtle" onClick={() => router.back()}> name: file.name,
<IconArrowBack color={colors['blue-button']} size={25} /> });
</Button>
</Box> const uploaded = res.data?.data;
if (!uploaded?.id) {
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> return toast.error("Gagal upload gambar");
<Stack gap="xs"> }
<Title order={3}>Create Kontak Darurat</Title>
kontakDaruratState.create.form.imageId = uploaded.id;
<TextInput
value={kontakDaruratState.create.form.name} await kontakDaruratState.create.create();
onChange={(val) => {
kontakDaruratState.create.form.name = val.target.value; resetForm();
}} router.push("/admin/kesehatan/kontak-darurat")
label={<Text fz="sm" fw="bold">Judul</Text>} }
placeholder="masukkan judul" return (
/> <Box>
<Box mb={10}>
<Box> <Button variant="subtle" onClick={() => router.back()}>
<Text fz="sm" fw="bold">Deskripsi</Text> <IconArrowBack color={colors['blue-button']} size={25} />
<CreateEditor </Button>
value={kontakDaruratState.create.form.deskripsi}
onChange={(val) => {
kontakDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box> </Box>
);
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Kontak Darurat</Title>
<TextInput
value={kontakDaruratState.create.form.name}
onChange={(val) => {
kontakDaruratState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={kontakDaruratState.create.form.deskripsi}
onChange={(val) => {
kontakDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
} }
export default CreateKontakDarurat; export default CreateKontakDarurat;

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -105,28 +106,61 @@ function EditPenangananDarurat() {
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box> </Box>
<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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -79,31 +80,65 @@ function CreatePenangananDarurat() {
}} }}
/> />
</Box> </Box>
<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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? ( {/* Tampilkan preview kalau ada */}
<Image alt="" src={previewImage} w={200} h={200} /> {previewImage && (
) : ( <Box mt="sm">
<Center w={200} h={200} bg="gray"> <Image
<IconImageInPicture /> src={previewImage}
</Center> alt="Preview"
)} style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
Simpan </Box>
</Button> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu'; import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -24,6 +25,7 @@ function EditPosyandu() {
nomor: statePosyandu.edit.form.nomor || '', nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '', deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '', imageId: statePosyandu.edit.form.imageId || '',
jadwalPelayanan: statePosyandu.edit.form.jadwalPelayanan || '',
}); });
useEffect(() => { useEffect(() => {
@@ -39,6 +41,7 @@ function EditPosyandu() {
nomor: data.nomor || '', nomor: data.nomor || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
imageId: data.imageId || '', imageId: data.imageId || '',
jadwalPelayanan: data.jadwalPelayanan || '',
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -61,6 +64,7 @@ function EditPosyandu() {
nomor: formData.nomor, nomor: formData.nomor,
deskripsi: formData.deskripsi, deskripsi: formData.deskripsi,
imageId: formData.imageId, imageId: formData.imageId,
jadwalPelayanan: formData.jadwalPelayanan,
} }
if (file) { if (file) {
@@ -94,25 +98,62 @@ function EditPosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title> <Title order={4}>Edit Posyandu</Title>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz={"md"} fw={"bold"}>Gambar</Text>
) : ( <Box>
<Center w={200} h={200} bg={"gray"}> <Dropzone
<IconImageInPicture /> onDrop={(files) => {
</Center> const selectedFile = files[0]; // Ambil file pertama
)} if (selectedFile) {
<FileInput setFile(selectedFile);
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
value={file} }
onChange={async (e) => { }}
if (!e) return; onReject={() => toast.error('File tidak valid.')}
setFile(e); maxSize={5 * 1024 ** 2} // Maks 5MB
const base64 = await e.arrayBuffer().then((buf) => accept={{ 'image/*': [] }}
"data:image/png;base64," + Buffer.from(buf).toString("base64") >
); <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
setPreviewImage(base64); <Dropzone.Accept>
}} <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
/> </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
@@ -135,6 +176,16 @@ function EditPosyandu() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Jadwal Pelayanan</Text>
<EditEditor
value={formData.jadwalPelayanan}
onChange={(htmlContent) => {
setFormData({ ...formData, jadwalPelayanan: htmlContent });
statePosyandu.edit.form.jadwalPelayanan = htmlContent;
}}
/>
</Box>
<Group> <Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group> </Group>

View File

@@ -63,6 +63,10 @@ function DetailPosyandu() {
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text> <Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.deskripsi }} /> <Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.deskripsi }} />
</Box> </Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jadwal Pelayanan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.jadwalPelayanan }} />
</Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePosyandu.findUnique.data.image?.link} alt="gambar" /> <Image src={statePosyandu.findUnique.data.image?.link} alt="gambar" />

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -22,6 +23,7 @@ function CreatePosyandu() {
nomor: "", nomor: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
jadwalPelayanan: "",
}; };
setFile(null); setFile(null);
@@ -65,25 +67,62 @@ function CreatePosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={4}>Create Posyandu</Title> <Title order={4}>Create Posyandu</Title>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz={"md"} fw={"bold"}>Gambar</Text>
) : ( <Box>
<Center w={200} h={200} bg={"gray"}> <Dropzone
<IconImageInPicture /> onDrop={(files) => {
</Center> const selectedFile = files[0]; // Ambil file pertama
)} if (selectedFile) {
<FileInput setFile(selectedFile);
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
value={file} }
onChange={async (e) => { }}
if (!e) return; onReject={() => toast.error('File tidak valid.')}
setFile(e); maxSize={5 * 1024 ** 2} // Maks 5MB
const base64 = await e.arrayBuffer().then((buf) => accept={{ 'image/*': [] }}
"data:image/png;base64," + Buffer.from(buf).toString("base64") >
); <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
setPreviewImage(base64); <Dropzone.Accept>
}} <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
/> </Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>} label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu' placeholder='Masukkan nama posyandu'
@@ -109,6 +148,15 @@ function CreatePosyandu() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Jadwal Pelayanan</Text>
<CreateEditor
value={statePosyandu.create.form.jadwalPelayanan}
onChange={(htmlContent) => {
statePosyandu.create.form.jadwalPelayanan = htmlContent;
}}
/>
</Box>
<Group> <Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group> </Group>

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,19 +30,21 @@ function ListPosyandu({ search }: { search: string }) {
const statePosyandu = useProxy(posyandustate) const statePosyandu = useProxy(posyandustate)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = statePosyandu.findMany;
useShallowEffect(() => { useShallowEffect(() => {
statePosyandu.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (statePosyandu.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.nomor.toString().toLowerCase().includes(keyword)
);
});
if (!statePosyandu.findMany.data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -70,10 +72,20 @@ function ListPosyandu({ search }: { search: string }) {
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.nomor}</TableTd>
<TableTd> <TableTd>
<Text fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Box w={100}>
<Text truncate="end" lineClamp={1} fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={100}>
<Text truncate="end" lineClamp={1} fz={"sm"}>{item.nomor}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={100}>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}> <Button onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}>
@@ -86,6 +98,15 @@ function ListPosyandu({ search }: { search: string }) {
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -91,57 +92,90 @@ function EditProgramKesehatan() {
</Button> </Button>
</Box> </Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Program Kesehatan</Title> <Title order={3}>Edit Program Kesehatan</Title>
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box>
<FileInput <TextInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} value={formData.deskripsiSingkat}
value={file} onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
onChange={async (e) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
if (!e) return; placeholder="masukkan deskripsi"
setFile(e); />
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? ( <Box>
<Image alt="" src={previewImage} w={200} h={200} /> <Text fz="sm" fw="bold">Deskripsi</Text>
) : ( <EditEditor
<Center w={200} h={200} bg="gray"> value={formData.deskripsi}
<IconImageInPicture /> onChange={(val) => setFormData({ ...formData, deskripsi: val })}
</Center> />
)} </Box>
<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>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <div>
Simpan <Text size="xl" inline>
</Button> Drag gambar ke sini atau klik untuk pilih file
</Stack> </Text>
</Paper> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack> </Stack>
</Box > </Box >
); );

View File

@@ -1,14 +1,15 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
import { Dropzone } from '@mantine/dropzone';
function CreateProgramKesehatan() { function CreateProgramKesehatan() {
const router = useRouter(); const router = useRouter();
@@ -94,28 +95,61 @@ function CreateProgramKesehatan() {
}} }}
/> />
</Box> </Box>
<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>
<FileInput <div>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text size="xl" inline>
value={file} Drag gambar ke sini atau klik untuk pilih file
onChange={async (e) => { </Text>
if (!e) return; <Text size="sm" c="dimmed" inline mt={7}>
setFile(e); Maksimal 5MB dan harus format gambar
const base64 = await e.arrayBuffer().then((buf) => </Text>
'data:image/png;base64,' + Buffer.from(buf).toString('base64') </div>
); </Group>
setPreviewImage(base64); </Dropzone>
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Simpan
</Button> </Button>

View File

@@ -4,8 +4,9 @@
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { ChangeEvent, useEffect, useState } from 'react'; import { ChangeEvent, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -141,22 +142,6 @@ function EditPuskesmas() {
} }
}; };
const handleFileChange = (selectedFile: File | null) => {
if (selectedFile) {
setFile(selectedFile);
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
setPreviewImage(e.target.result as string);
}
};
reader.readAsDataURL(selectedFile);
} else {
setFile(null);
setPreviewImage(null);
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ setFormData(prev => ({
@@ -186,7 +171,7 @@ function EditPuskesmas() {
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Puskesmas</Title> <Title order={3}>Edit Puskesmas</Title>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>} label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>}
placeholder="masukkan nama puskesmas" placeholder="masukkan nama puskesmas"
@@ -252,26 +237,66 @@ function EditPuskesmas() {
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/> />
<FileInput <Box>
placeholder="Pilih gambar" <Text fz={"md"} fw={"bold"}>Gambar</Text>
label="Gambar" <Box>
accept="image/*" <Dropzone
leftSection={<IconImageInPicture size={16} />} onDrop={(files) => {
value={file} const selectedFile = files[0]; // Ambil file pertama
onChange={handleFileChange} 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>
{previewImage ? ( <div>
<Image alt="Preview" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg="gray"> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
<Button {/* Tampilkan preview kalau ada */}
onClick={handleSubmit} {previewImage && (
bg={colors['blue-button']} <Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
loading={statePuskesmas.edit.loading} loading={statePuskesmas.edit.loading}
> >
Simpan Perubahan Simpan Perubahan

View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -38,7 +39,7 @@ function CreatePuskesmas() {
setFile(null); setFile(null);
setPreviewImage(null); setPreviewImage(null);
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@@ -144,7 +145,7 @@ function CreatePuskesmas() {
statePuskesmas.create.form.kontak.facebook = e.target.value; statePuskesmas.create.form.kontak.facebook = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>} label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak ugd" placeholder="masukkan kontak ugd"
value={statePuskesmas.create.form.kontak.kontakUGD} value={statePuskesmas.create.form.kontak.kontakUGD}
@@ -153,27 +154,61 @@ function CreatePuskesmas() {
}} }}
/> />
<FileInput <Box>
label={<Text fz="sm" fw="bold">Upload Gambar</Text>} <Text fz={"md"} fw={"bold"}>Gambar</Text>
value={file} <Box>
onChange={async (e) => { <Dropzone
if (!e) return; onDrop={(files) => {
setFile(e); const selectedFile = files[0]; // Ambil file pertama
const base64 = await e.arrayBuffer().then((buf) => if (selectedFile) {
'data:image/png;base64,' + Buffer.from(buf).toString('base64') setFile(selectedFile);
); setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(base64); }
}} }}
/> 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>
{previewImage ? ( <div>
<Image alt="" src={previewImage} w={200} h={200} /> <Text size="xl" inline>
) : ( Drag gambar ke sini atau klik untuk pilih file
<Center w={200} h={200} bg="gray"> </Text>
<IconImageInPicture /> <Text size="sm" c="dimmed" inline mt={7}>
</Center> Maksimal 5MB dan harus format gambar
)} </Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Puskesmas Simpan Puskesmas
</Button> </Button>

View File

@@ -36,6 +36,10 @@ function Page() {
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]); const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
useShallowEffect(() => { useShallowEffect(() => {
if (!data && !loading) {
state.findMany.load();
return;
}
if (data) { if (data) {
// Hitung total berdasarkan jenis kelamin // Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length; const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;

View File

@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID'; import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Biodata from './biodata/biodataForm'; import Biodata from './biodata/biodataForm';
@@ -58,21 +59,6 @@ function EditProfilePPID() {
stateProfilePPID.editForm.updateField(field as any, value); stateProfilePPID.editForm.updateField(field as any, value);
}; };
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !stateProfilePPID.editForm.form.name.trim()) { if (isSubmitting || !stateProfilePPID.editForm.form.name.trim()) {
toast.error("Nama wajib diisi"); toast.error("Nama wajib diisi");
@@ -183,26 +169,61 @@ function EditProfilePPID() {
/> />
{/* File Upload */} {/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box> <Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
{previewImage ? ( <Box>
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" /> <Dropzone
) : ( onDrop={(files) => {
<Center w={200} h={200} bg="gray.2"> const selectedFile = files[0]; // Ambil file pertama
<Stack align="center" gap="xs"> if (selectedFile) {
<IconImageInPicture size={48} color="gray" /> setFile(selectedFile);
<Text size="sm" c="gray">Tidak ada gambar</Text> setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
</Stack> }
</Center> }}
)} onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
{/* Rich Components */} {/* Rich Components */}

View File

@@ -93,12 +93,15 @@ function EditPosisiOrganisasiPPID() {
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>} label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
placeholder='Masukkan nama posisi organisasi' placeholder='Masukkan nama posisi organisasi'
/> />
<EditEditor <Box>
value={formData.deskripsi} <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
onChange={(htmlContent) => { <EditEditor
setFormData({ ...formData, deskripsi: htmlContent }); value={formData.deskripsi}
}} onChange={(htmlContent) => {
/> setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<TextInput <TextInput
value={formData.hierarki} value={formData.hierarki}
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })} onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}

View File

@@ -3,7 +3,7 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -46,12 +46,15 @@ function CreatePosisiOrganisasiPPID() {
value={stateOrganisasi.create.form.nama} value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
/> />
<CreateEditor <Box>
value={stateOrganisasi.create.form.deskripsi} <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
onChange={(htmlContent) => { <CreateEditor
stateOrganisasi.create.form.deskripsi = htmlContent; value={stateOrganisasi.create.form.deskripsi}
}} onChange={(htmlContent) => {
/> stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput <TextInput
label="Hierarki" label="Hierarki"
type="number" type="number"

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
specialist: string;
jadwal: string;
};
export default async function dokterTenagaMedisCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.dokterdanTenagaMedis.create({
data: {
name: body.name,
specialist: body.specialist,
jadwal: body.jadwal,
},
select: {
name: true,
specialist: true,
jadwal: true,
}
});
return {
success: true,
message: "Success create dokter tenaga medis",
data: created,
};
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dokterTenagaMedisDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
};
}
const existing = await prisma.dokterdanTenagaMedis.findUnique({
where: {
id: id,
},
});
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
};
}
const deleted = await prisma.dokterdanTenagaMedis.delete({
where: { id },
});
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
};
}

View File

@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function dokterTenagaMedisFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ specialist: { contains: search, mode: 'insensitive' } },
{ jadwal: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.dokterdanTenagaMedis.findMany({
where,
skip,
take: limit,
orderBy: { name: 'asc' },
}),
prisma.dokterdanTenagaMedis.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data dokter tenaga medis dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data dokter tenaga medis",
};
}
}
export default dokterTenagaMedisFindMany;

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
export default async function dokterTenagaMedisFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: 'ID tidak boleh kosong',
}, {status: 400})
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.dokterdanTenagaMedis.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Berhasil mengambil data berdasarkan ID",
data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,32 @@
import Elysia, { t } from "elysia";
import dokterTenagaMedisCreate from "./create";
import dokterTenagaMedisFindMany from "./findMany";
import dokterTenagaMedisFindUnique from "./findUnique";
import dokterTenagaMedisUpdate from "./updt";
import dokterTenagaMedisDelete from "./del";
const DokterTenagaMedis = new Elysia({
prefix: "/doktertenagamedis",
tags: ["Data Kesehatan/Fasilitas Kesehatan/Dokter Tenaga Medis"]
})
.get("/:id", async (context) => {
const response = await dokterTenagaMedisFindUnique(new Request(context.request));
return response;
})
.get("/findMany", dokterTenagaMedisFindMany)
.post("/create", dokterTenagaMedisCreate, {
body: t.Object({
name: t.String(),
specialist: t.String(),
jadwal: t.String(),
}),
})
.put("/:id", dokterTenagaMedisUpdate, {
body: t.Object({
name: t.String(),
specialist: t.String(),
jadwal: t.String(),
}),
})
.delete("/del/:id", dokterTenagaMedisDelete)
export default DokterTenagaMedis

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
specialist: string;
jadwal: string;
}
export default async function dokterTenagaMedisUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.dokterdanTenagaMedis.update({
where: { id },
data: {
name: body.name,
specialist: body.specialist,
jadwal: body.jadwal,
},
});
return {
success: true,
message: "Berhasil mengupdate data dokter tenaga medis",
data: result,
};
} catch (error) {
console.error("Error updating data dokter tenaga medis:", error);
throw new Error("Gagal mengupdate data dokter tenaga medis: " + (error as Error).message);
}
}

View File

@@ -1,25 +1,31 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia"; import { Context } from "elysia";
type FormCreate = Prisma.GrafikKepuasanGetPayload<{ type FormCreate = {
select: { nama: string;
label: true; tanggal: string;
jumlah: true jenisKelamin: string;
}; alamat: string;
}>; penyakit: string;
};
export default async function grafikKepuasanCreate(context: Context) { export default async function grafikKepuasanCreate(context: Context) {
const body = context.body as FormCreate; const body = context.body as FormCreate;
const created = await prisma.grafikKepuasan.create({ const created = await prisma.grafikKepuasan.create({
data: { data: {
label: body.label, nama: body.nama,
jumlah: body.jumlah, tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
penyakit: body.penyakit,
}, },
select: { select: {
id: true, nama: true,
label: true, tanggal: true,
jumlah: true, jenisKelamin: true,
alamat: true,
penyakit: true,
} }
}); });
return { return {

View File

@@ -1,8 +1,56 @@
import prisma from "@/lib/prisma" /* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function grafikKepuasanFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
{ jenisKelamin: { contains: search, mode: 'insensitive' } },
{ penyakit: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.grafikKepuasan.findMany({
where,
skip,
take: limit,
orderBy: { nama: 'asc' },
}),
prisma.grafikKepuasan.count({ where }),
]);
export default async function grafikKepuasanFindMany() {
const res = await prisma.grafikKepuasan.findMany()
return { return {
data: res success: true,
} message: "Berhasil ambil data grafik kepuasan dengan pagination",
} data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data grafik kepuasan",
};
}
}
export default grafikKepuasanFindMany;

View File

@@ -16,22 +16,21 @@ const GrafikKepuasan = new Elysia({
.get("/find-many", grafikKepuasanFindMany) .get("/find-many", grafikKepuasanFindMany)
.post("/create", grafikKepuasanCreate, { .post("/create", grafikKepuasanCreate, {
body: t.Object({ body: t.Object({
label: t.String(), nama: t.String(),
jumlah: t.String(), tanggal: t.String(),
jenisKelamin: t.String(),
alamat: t.String(),
penyakit: t.String(),
}), }),
}) })
.put("/:id", grafikKepuasanUpdate, { .put("/:id", grafikKepuasanUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({ body: t.Object({
label: t.String(), nama: t.String(),
jumlah: t.String(), tanggal: t.String(),
}), jenisKelamin: t.String(),
}) alamat: t.String(),
.delete("/del/:id", grafikKepuasanDelete, { penyakit: t.String(),
params: t.Object({
id: t.String(),
}), }),
}) })
.delete("/del/:id", grafikKepuasanDelete)
export default GrafikKepuasan export default GrafikKepuasan

View File

@@ -1,45 +1,36 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
type FormUpdate = {
nama: string;
tanggal: string;
jenisKelamin: string;
alamat: string;
penyakit: string;
}
export default async function grafikKepuasanUpdate(context: Context) { export default async function grafikKepuasanUpdate(context: Context) {
const id = context.params?.id; const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
if (!id) {
try {
const result = await prisma.grafikKepuasan.update({
where: { id },
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
penyakit: body.penyakit,
},
});
return { return {
success: false, success: true,
message: "ID tidak ditemukan" message: "Berhasil mengupdate data grafik kepuasan",
} data: result,
} };
} catch (error) {
const {label, jumlah} = context.body as { console.error("Error updating data grafik kepuasan:", error);
label: string; throw new Error("Gagal mengupdate data grafik kepuasan: " + (error as Error).message);
jumlah: string;
}
const existing = await prisma.grafikKepuasan.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const updated = await prisma.grafikKepuasan.update({
where: { id },
data: {
label,
jumlah,
},
})
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
} }
} }

View File

@@ -1,32 +1,22 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia"; import { Context } from "elysia";
type FormCreate = Prisma.DataKematian_KelahiranGetPayload<{ type FormCreate = {
select: { kematianId: string;
tahun: true; kelahiranId: string;
kematianKasar: true; };
kematianBayi: true;
kelahiranKasar: true;
};
}>;
export default async function persentaseKelahiranKematianCreate(context: Context) { export default async function persentaseKelahiranKematianCreate(context: Context) {
const body = context.body as FormCreate const body = context.body as FormCreate
const created = await prisma.dataKematian_Kelahiran.create({ const created = await prisma.dataKematian_Kelahiran.create({
data: { data: {
tahun: body.tahun, kematianId: body.kematianId,
kematianKasar: body.kematianKasar, kelahiranId: body.kelahiranId,
kematianBayi: body.kematianBayi,
kelahiranKasar: body.kelahiranKasar,
}, },
select: { select: {
id: true, kematianId: true,
tahun: true, kelahiranId: true,
kematianKasar: true,
kematianBayi: true,
kelahiranKasar: true,
} }
}) })
return{ return{

View File

@@ -1,9 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function persentaseKelahiranKematianFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ kematian: { nama: { contains: search, mode: 'insensitive' } } },
{ kelahiran: { nama: { contains: search, mode: 'insensitive' } } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.dataKematian_Kelahiran.findMany({
where,
include: {
kematian: true,
kelahiran: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.dataKematian_Kelahiran.count({ where }),
]);
export default async function persentaseKelahiranKematianFindMany() {
const res = await prisma.dataKematian_Kelahiran.findMany();
return { return {
data: res success: true,
} message: "Berhasil ambil data persentase kelahiran kematian dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data persentase kelahiran kematian",
};
}
} }
export default persentaseKelahiranKematianFindMany;

View File

@@ -22,6 +22,10 @@ export default async function persentaseKelahiranKematianFindUnique(request: Req
const data = await prisma.dataKematian_Kelahiran.findUnique({ const data = await prisma.dataKematian_Kelahiran.findUnique({
where: { id }, where: { id },
include: {
kematian: true,
kelahiran: true,
},
}); });
if (!data) { if (!data) {

View File

@@ -16,10 +16,8 @@ const PersentaseKelahiranKematian = new Elysia({
.get("/find-many", persentaseKelahiranKematianFindMany) .get("/find-many", persentaseKelahiranKematianFindMany)
.post("/create", persentaseKelahiranKematianCreate, { .post("/create", persentaseKelahiranKematianCreate, {
body: t.Object({ body: t.Object({
tahun: t.String(), kematianId: t.String(),
kematianKasar: t.String(), kelahiranId: t.String(),
kematianBayi: t.String(),
kelahiranKasar: t.String(),
}), }),
}) })
.put("/:id", persentaseKelahiranKematianUpdate, { .put("/:id", persentaseKelahiranKematianUpdate, {
@@ -27,10 +25,8 @@ const PersentaseKelahiranKematian = new Elysia({
id: t.String(), id: t.String(),
}), }),
body: t.Object({ body: t.Object({
tahun: t.String(), kematianId: t.String(),
kematianKasar: t.String(), kelahiranId: t.String(),
kematianBayi: t.String(),
kelahiranKasar: t.String(),
}), }),
}) })
.delete("/del/:id", persentaseKelahiranKematianDelete, { .delete("/del/:id", persentaseKelahiranKematianDelete, {

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
tanggal: string;
jenisKelamin: string;
alamat: string;
}
export default async function kelahiranCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.kelahiran.create({
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
},
select: {
nama: true,
tanggal: true,
jenisKelamin: true,
alamat: true,
},
});
return {
success: true,
message: "Success create kelahiran",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kelahiranDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.kelahiran.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.kelahiran.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function kelahiranFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.kelahiran.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.kelahiran.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data kelahiran dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data kelahiran",
};
}
}
export default kelahiranFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function kelahiranFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.kelahiran.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get data kelahiran",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import kelahiranCreate from "./create";
import kelahiranDelete from "./del";
import kelahiranFindMany from "./findMany";
import kelahiranFindUnique from "./findUnique";
import kelahiranUpdate from "./updt";
const Kelahiran = new Elysia({
prefix: "/kelahiran",
tags: ["Kesehatan / Data Kesehatan Warga / Persentase Kelahiran Kematian / Kelahiran"],
})
.post("/create", kelahiranCreate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
jenisKelamin: t.String(),
alamat: t.String(),
}),
})
.get("/findMany", kelahiranFindMany)
.get("/:id", async (context) => {
const response = await kelahiranFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", kelahiranUpdate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
jenisKelamin: t.String(),
alamat: t.String(),
}),
})
.delete("/del/:id", kelahiranDelete);
export default Kelahiran;

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
nama: string;
tanggal: string;
jenisKelamin: string;
alamat: string;
}
export default async function kelahiranUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.kelahiran.update({
where: { id },
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
},
});
return {
success: true,
message: "Berhasil mengupdate data kelahiran",
data: result,
};
} catch (error) {
console.error("Error updating data kelahiran:", error);
throw new Error("Gagal mengupdate data kelahiran: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
tanggal: string;
jenisKelamin: string;
alamat: string;
penyebab: string;
};
export default async function kematianCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.kematian.create({
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
penyebab: body.penyebab,
},
select: {
nama: true,
tanggal: true,
jenisKelamin: true,
alamat: true,
penyebab: true,
},
});
return {
success: true,
message: "Success create kematian",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kematianDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.kematian.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.kematian.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function kematianFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.kematian.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.kematian.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data kematian dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data kematian",
};
}
}
export default kematianFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function kematianFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.kematian.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get data kematian",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,41 @@
import Elysia, { t } from "elysia";
import kematianCreate from "./create";
import kematianDelete from "./del";
import kematianFindMany from "./findMany";
import kematianFindUnique from "./findUnique";
import kematianUpdate from "./updt";
const Kematian = new Elysia({
prefix: "/kematian",
tags: ["Kesehatan / Data Kesehatan Warga / Persentase Kelahiran Kematian / Kematian"],
})
.post("/create", kematianCreate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
jenisKelamin: t.String(),
alamat: t.String(),
penyebab: t.String(),
}),
})
.get("/findMany", kematianFindMany)
.get("/:id", async (context) => {
const response = await kematianFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", kematianUpdate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
jenisKelamin: t.String(),
alamat: t.String(),
penyebab: t.String(),
}),
})
.delete("/del/:id", kematianDelete);
export default Kematian;

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
nama: string;
tanggal: string;
jenisKelamin: string;
alamat: string;
penyebab: string;
}
export default async function kematianUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.kematian.update({
where: { id },
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
jenisKelamin: body.jenisKelamin,
alamat: body.alamat,
penyebab: body.penyebab,
},
});
return {
success: true,
message: "Berhasil mengupdate data kematian",
data: result,
};
} catch (error) {
console.error("Error updating data kematian:", error);
throw new Error("Gagal mengupdate data kematian: " + (error as Error).message);
}
}

View File

@@ -11,11 +11,9 @@ export default async function persentaseKelahiranKematianUpdate(context: Context
} }
} }
const {tahun, kematianKasar, kematianBayi, kelahiranKasar} = context.body as { const {kematianId, kelahiranId} = context.body as {
tahun: string; kematianId: string;
kematianKasar: string; kelahiranId: string;
kematianBayi: string;
kelahiranKasar: string;
} }
const existing = await prisma.dataKematian_Kelahiran.findUnique({ const existing = await prisma.dataKematian_Kelahiran.findUnique({
@@ -34,10 +32,8 @@ export default async function persentaseKelahiranKematianUpdate(context: Context
const updated = await prisma.dataKematian_Kelahiran.update({ const updated = await prisma.dataKematian_Kelahiran.update({
where: { id }, where: { id },
data: { data: {
tahun, kematianId,
kematianKasar, kelahiranId,
kematianBayi,
kelahiranKasar,
}, },
}) })

View File

@@ -16,6 +16,9 @@ import Puskesmas from "./puskesmas";
import FasilitasKesehatan from "./data_kesehatan_warga/fasilitas_kesehatan"; import FasilitasKesehatan from "./data_kesehatan_warga/fasilitas_kesehatan";
import JadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan"; import JadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan";
import ArtikelKesehatan from "./data_kesehatan_warga/artikel_kesehatan"; import ArtikelKesehatan from "./data_kesehatan_warga/artikel_kesehatan";
import Kelahiran from "./data_kesehatan_warga/persentase_kelahiran_kematian/kelahiran";
import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian";
import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis";
const Kesehatan = new Elysia({ const Kesehatan = new Elysia({
@@ -38,5 +41,8 @@ const Kesehatan = new Elysia({
.use(InfoWabahPenyakit) .use(InfoWabahPenyakit)
.use(FasilitasKesehatan) .use(FasilitasKesehatan)
.use(JadwalKegiatan) .use(JadwalKegiatan)
.use(ArtikelKesehatan); .use(ArtikelKesehatan)
.use(Kelahiran)
.use(Kematian)
.use(DokterTenagaMedis)
export default Kesehatan; export default Kesehatan;

View File

@@ -8,6 +8,7 @@ type FormCreate = Prisma.PosyanduGetPayload<{
nomor: true; nomor: true;
deskripsi: true; deskripsi: true;
imageId: true; imageId: true;
jadwalPelayanan: true;
}; };
}>; }>;
export default async function posyanduCreate(context: Context) { export default async function posyanduCreate(context: Context) {
@@ -19,6 +20,7 @@ export default async function posyanduCreate(context: Context) {
nomor: body.nomor, nomor: body.nomor,
deskripsi: body.deskripsi, deskripsi: body.deskripsi,
imageId: body.imageId, imageId: body.imageId,
jadwalPelayanan: body.jadwalPelayanan,
} }
}) })
return { return {

View File

@@ -1,26 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function posyanduFindMany() { async function posyanduFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.posyandu.findMany({ const page = Number(context.query.page) || 1;
where: { const limit = Number(context.query.limit) || 10;
isActive: true, const search = (context.query.search as string) || '';
}, const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
{ nomor: { contains: search, mode: 'insensitive' } },
{ jadwalPelayanan: { contains: search, mode: 'insensitive' } }
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.posyandu.findMany({
where,
include: { include: {
image: true, image: true,
} },
}) skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.posyandu.count({ where }),
]);
return { return {
success: true, success: true,
message: "Success fetch posyandu", message: "Berhasil ambil posyandu dengan pagination",
data, data,
} page,
} catch (error) { limit,
console.error("Find many error:", error); total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di posyanduFindMany paginated:", e);
return { return {
success: false, success: false,
message: "Failed fetch posyandu", message: "Gagal mengambil data posyandu",
} };
}
} }
} export default posyanduFindMany;

View File

@@ -15,6 +15,7 @@ const Posyandu = new Elysia({
nomor: t.String(), nomor: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
imageId: t.String(), imageId: t.String(),
jadwalPelayanan: t.String(),
}) })
}) })
.get("/find-many", posyanduFindMany) .get("/find-many", posyanduFindMany)
@@ -35,6 +36,7 @@ const Posyandu = new Elysia({
nomor: t.String(), nomor: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
imageId: t.String(), imageId: t.String(),
jadwalPelayanan: t.String(),
}) })
} }
) )

View File

@@ -11,6 +11,7 @@ type FormUpdate = Prisma.PosyanduGetPayload<{
nomor: true; nomor: true;
deskripsi: true; deskripsi: true;
imageId: true; imageId: true;
jadwalPelayanan: true;
} }
}> }>
@@ -24,6 +25,7 @@ export default async function posyanduUpdate(context: Context) {
nomor, nomor,
deskripsi, deskripsi,
imageId, imageId,
jadwalPelayanan,
} = body; } = body;
if(!id) { if(!id) {
@@ -79,6 +81,7 @@ export default async function posyanduUpdate(context: Context) {
nomor, nomor,
deskripsi, deskripsi,
imageId, imageId,
jadwalPelayanan,
} }
}) })

View File

@@ -0,0 +1,132 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import grafikkepuasan from "@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan";
import colors from "@/con/colors";
import { Box, Center, Paper, Skeleton, Stack, Text, Title } from "@mantine/core";
import { useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { useEffect, useState } from "react";
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from "recharts";
import { useProxy } from "valtio/utils";
function GrafikPenyakit() {
type PDKMGRAFIK = {
id: string;
nama: string;
tanggal: string | Date; // Allow both string and Date types
jenisKelamin: string;
alamat: string;
penyakit: string;
createdAt?: Date; // Add optional fields that might come from the API
updatedAt?: Date;
deletedAt?: Date | null;
}
const statePenyakit = useProxy(grafikkepuasan)
const [chartData, setChartData] = useState<PDKMGRAFIK[]>([])
const [mounted, setMounted] = useState(false)
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const {
data,
page,
loading,
load,
} = statePenyakit.findMany
useShallowEffect(() => {
setMounted(true)
load(page, 10)
}, [page])
useEffect(() => {
setMounted(true)
if (data) {
setChartData(data.map((item) => ({
id: item.id,
nama: item.nama,
tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal,
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
})))
}
}, [data])
const processDiseaseData = (data: PDKMGRAFIK[]) => {
const diseaseCount: Record<string, number> = {};
data.forEach(item => {
const penyakit = item.penyakit.trim();
if (penyakit) {
diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
}
});
return Object.entries(diseaseCount).map(([name, count]) => ({
name,
count
}));
};
// Add this state to store the processed chart data
const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]);
// Update the chart data when data changes
useEffect(() => {
if (data && data.length > 0) {
setDiseaseChartData(processDiseaseData(data));
}
}, [data]);
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Center>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Center>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors["white-trans-1"]} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 && (
<Center>
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} >
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Penderita" />
</BarChart>
</Center>
)}
</Paper>
</Box>
)}
</Box>
);
}
export default GrafikPenyakit;

View File

@@ -1,49 +1,95 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, Paper, Center, Flex, ColorSwatch, SimpleGrid, Anchor, Divider, Image } from '@mantine/core'; import { BarChart as MantineBarChart } from '@mantine/charts';
import React from 'react'; import { Anchor, Box, Center, ColorSwatch, Divider, Flex, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts';
import Link from 'next/link'; import Link from 'next/link';
// import { useRouter } from 'next/navigation';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import { useProxy } from 'valtio/utils';
import GrafikPenyakit from './grafik-penyakit/page';
const dataKematian = [
{
id: 1,
tahun: '2023',
kematianKasar: '1.7',
kematianBayi: '1.4',
kelahiranKasar: '0.5'
},
{
id: 2,
tahun: '2024',
kematianKasar: '1.4',
kematianBayi: '1.8',
kelahiranKasar: '1.5'
},
{
id: 3,
tahun: '2025',
kematianKasar: '2.0',
kematianBayi: '1.5',
kelahiranKasar: '1.2'
},
]
const dataPenyakit = [
{ penyakit: 'Covid', penderita: 335 },
{ penyakit: 'Tuli', penderita: 105 },
{ penyakit: 'Bisul', penderita: 98 },
{ penyakit: 'Panas', penderita: 96 },
{ penyakit: 'Batuk', penderita: 87 },
{ penyakit: 'Sembelit', penderita: 72 },
{ penyakit: 'Demam', penderita: 51 },
{ penyakit: 'Gred', penderita: 36 },
{ penyakit: 'Magh', penderita: 34 },
{ penyakit: 'Farangitis Akut', penderita: 17 },
]
function Page() { function Page() {
type DataTahunan = {
tahun: string;
totalKelahiran: number;
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
// Count occurrences per year
const countByYear = (data: any[], dateField: string) => {
const counts: Record<string, number> = {};
data?.forEach(item => {
const year = new Date(item[dateField]).getFullYear().toString();
counts[year] = (counts[year] || 0) + 1;
});
return counts;
};
const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<DataTahunan[]>([]);
const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)');
useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000); // Load all kelahiran data
statePersentase.kematian.findMany.load(1, 1000); // Load all kematian data
}, []);
useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
// Count kelahiran and kematian by year
const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal');
const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal');
// Get all unique years
const allYears = new Set([
...Object.keys(kelahiranByYear),
...Object.keys(kematianByYear)
]);
// Create data structure for the chart
const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => {
acc[year] = {
tahun: year,
totalKelahiran: kelahiranByYear[year] || 0,
totalKematian: kematianByYear[year] || 0,
data: []
};
return acc;
}, {});
const sortedData = Object.values(dataByYear).sort((a, b) =>
parseInt(a.tahun) - parseInt(b.tahun)
);
setChartData(sortedData);
}
}, [
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data,
]);
if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -61,63 +107,50 @@ function Page() {
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}> <Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
<Box> <Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}> <Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian Kasar</Text> <Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
<ColorSwatch color="#26308A" size={30} /> <ColorSwatch color="#EF3E3E" size={30} />
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}> <Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian Bayi</Text> <Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
<ColorSwatch color="#135A9B" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran Kasar</Text>
<ColorSwatch color="#3290CA" size={30} /> <ColorSwatch color="#3290CA" size={30} />
</Flex> </Flex>
</Box> </Box>
</Flex> </Flex>
<Center> {chartData.length === 0 ? (
<BarChart <Text c="dimmed" ta="center" py="xl">
h={400} Belum ada data yang tersedia untuk ditampilkan
data={dataKematian} </Text>
dataKey="tahun" ) : (
series={[ <>
{ name: 'kematianKasar', color: '#26308A' }, {/* Main Chart */}
{ name: 'kematianBayi', color: '#135A9B' }, <Center>
{ name: 'kelahiranKasar', color: '#3290CA' }, <Box h={400}>
]} <Box style={{
tickLine="y" width: isMobile ? '90vw' : isTablet ? '700px' : '800px',
/> maxWidth: '100%',
</Center> margin: '0 auto'
</Box> }}>
</Paper> <MantineBarChart
</Box> h={350}
{/* Bar Chart Penyakit */} data={chartData}
<Box> dataKey="tahun"
<Paper p={"xl"} bg={colors['white-trans-1']}> series={[
<Box pb={30}> { name: 'totalKelahiran', label: 'Total Kelahiran', color: '#3290CA' },
<Text pb={30} fw={"bold"} fz={{ base: 'h4', md: 'h3' }} ta={"center"}> { name: 'totalKematian', label: 'Total Kematian', color: '#f03e3e' }
Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik ]}
</Text> tickLine="y"
<Center> />
<BarChart </Box>
p={20} </Box>
mb={30} </Center>
h={500} </>
data={dataPenyakit} )}
dataKey='penyakit'
orientation='vertical'
yAxisProps={{ width: 80 }}
barProps={{ radius: 10 }}
series={[{ name: 'penderita', color: colors['blue-button'] }]}
/>
</Center>
<Text ta={"center"} fw={"bold"} fz={"h4"}>Jumlah Penderita</Text>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
<GrafikPenyakit />
{/* Artikel Kesehatan */} {/* Artikel Kesehatan */}
<Box> <Box>
<SimpleGrid <SimpleGrid
@@ -193,7 +226,7 @@ function Page() {
</Text> </Text>
</Anchor> </Anchor>
</Box> </Box>
<Divider color={colors['blue-button']} px={'xl'}/> <Divider color={colors['blue-button']} px={'xl'} />
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
@@ -233,7 +266,7 @@ function Page() {
09:00-14:00 WITA 09:00-14:00 WITA
</Text> </Text>
<Text fz={'h4'}> <Text fz={'h4'}>
Puskesmas Abiansemal III Puskesmas Abiansemal III
</Text> </Text>
<Anchor component={Link} href={'/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan'} c={colors['blue-button']} variant='transparent'> <Anchor component={Link} href={'/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan'} c={colors['blue-button']} variant='transparent'>
<Text c={colors['blue-button']} fz={'h4'} > <Text c={colors['blue-button']} fz={'h4'} >
@@ -249,16 +282,16 @@ function Page() {
<Paper p={'xl'} h={'112vh'} bg={colors['white-trans-1']}> <Paper p={'xl'} h={'112vh'} bg={colors['white-trans-1']}>
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Box> <Box>
<Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text> <Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text>
<Image pt={5} src={'/api/img/dbd.png'} alt="" /> <Image pt={5} src={'/api/img/dbd.png'} alt="" />
<Text fz={'h4'} fw={'bold'} > <Text fz={'h4'} fw={'bold'} >
Tips Mencegah Demam Berdarah Saat Musim Hujan Tips Mencegah Demam Berdarah Saat Musim Hujan
</Text> </Text>
<Text fz={'h6'} pb={10}> <Text fz={'h6'} pb={10}>
Diposting: 12 Februari 2025 | Dinas Kesehatan Diposting: 12 Februari 2025 | Dinas Kesehatan
</Text> </Text>
<Text fz={'h4'} pb={10}> <Text fz={'h4'} pb={10}>
Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan. Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.
</Text> </Text>
<Anchor c={'black'} component={Link} href={'/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan'} variant='transparent'> <Anchor c={'black'} component={Link} href={'/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan'} variant='transparent'>
<Text c={colors['blue-button']} fz={'h4'} > <Text c={colors['blue-button']} fz={'h4'} >
@@ -268,16 +301,16 @@ function Page() {
</Box> </Box>
<Divider color={colors['blue-button']} px={'xl'} /> <Divider color={colors['blue-button']} px={'xl'} />
<Box> <Box>
<Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text> <Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text>
<Image pt={5} src={'/api/img/dbd.png'} alt="" /> <Image pt={5} src={'/api/img/dbd.png'} alt="" />
<Text fz={'h4'} fw={'bold'} > <Text fz={'h4'} fw={'bold'} >
Tips Mencegah Demam Berdarah Saat Musim Hujan Tips Mencegah Demam Berdarah Saat Musim Hujan
</Text> </Text>
<Text fz={'h6'} pb={10}> <Text fz={'h6'} pb={10}>
Diposting: 12 Februari 2025 | Dinas Kesehatan Diposting: 12 Februari 2025 | Dinas Kesehatan
</Text> </Text>
<Text fz={'h4'} pb={10}> <Text fz={'h4'} pb={10}>
Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan. Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.
</Text> </Text>
<Anchor c={'black'} href={'/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan'} variant='transparent'> <Anchor c={'black'} href={'/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan'} variant='transparent'>
<Text c={colors['blue-button']} fz={'h4'} > <Text c={colors['blue-button']} fz={'h4'} >

View File

@@ -1,36 +1,58 @@
'use client'
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Stack, Box, Text, SimpleGrid, Paper, Center, Image, Flex, List, ListItem } from "@mantine/core"; import { Box, Center, Flex, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
// import { useTransitionRouter } from "next-view-transitions";
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import { useShallowEffect } from "@mantine/hooks";
import { useProxy } from "valtio/utils";
import { useState } from "react";
import { IconSearch } from "@tabler/icons-react";
const data = [
{
id: 1,
judul: 'Posyandu Banjar Bucu',
nomor: '082345678910',
image: '/api/img/posyandu.png'
},
{
id: 2,
judul: 'Posyandu Banjar Bucu',
nomor: '082345678910',
image: '/api/img/posyandu.png'
},
{
id: 3,
judul: 'Posyandu Banjar Bucu',
nomor: '082345678910',
image: '/api/img/posyandu.png'
}
]
export default function Page() { export default function Page() {
const state = useProxy(posyandustate)
// const router = useTransitionRouter()
const [search, setSearch] = useState("")
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
<Flex mt={10} justify={"space-between"} align={"center"}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Posyandu Darmasaba
</Text>
<TextInput
placeholder="Cari Posyandu"
radius="lg"
leftSection={<IconSearch size={20} />}
w={{ base: "25%", md: "30%" }}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Flex>
</Box> </Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Posyandu Darmasaba
</Text>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Stack gap={'lg'}>
<SimpleGrid <SimpleGrid
@@ -39,49 +61,46 @@ export default function Page() {
base: 1, base: 1,
md: 3, md: 3,
}}> }}>
{data.map((v, k) => { {data?.map((v, k) => {
return ( return (
<Paper key={k} p={"xl"} bg={colors["white-trans-1"]}> <Paper key={k} p={"xl"} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Text c={colors["blue-button"]} fw={"bold"} fz={"h3"}> <Text c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul} {v.name}
</Text>
<Text fz={'h4'}>
{v.nomor}
</Text> </Text>
<Center> <Center>
<Image src={v.image} alt="" /> <Image src={v.image.link} alt="" />
</Center> </Center>
<Text fz={'h4'}> <Text fz={'h4'}>
Jadwal Pelayanan No.Telp : {v.nomor}
</Text>
<Text fz={'h4'}>
Setiap tanggal 5, Pukul 09:00 -
12:00 WITA
</Text> </Text>
<Box> <Box>
<Flex justify={'space-between'}> <Text fz={'h4'}>
<Text fz={'h4'}>Balita</Text> Jadwal Pelayanan
<Box> </Text>
<Text fz={'h4'}>Selasa minggu pertama</Text> <Text fz={'h4'} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
<Text fz={'h4'}>(09:00-11:00 WITA)</Text>
</Box>
</Flex>
</Box>
<Box>
<Flex justify={'space-between'}>
<Text fz={'h4'}>Lansia</Text>
<Box>
<Text fz={'h4'}>Selasa minggu pertama</Text>
<Text fz={'h4'}>(09:00-11:00 WITA)</Text>
</Box>
</Flex>
</Box> </Box>
<Spoiler
maxHeight={60} // tinggi maksimum sebelum di-collapse
showLabel="Read more"
hideLabel="Read less"
>
<Text fz={'h4'} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Spoiler>
</Stack> </Stack>
</Paper> </Paper>
) )
})} })}
</SimpleGrid> </SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
<Text fz={'h4'} fw={"bold"}> <Text fz={'h4'} fw={"bold"}>
Pelayanan Posyandu Pelayanan Posyandu
</Text> </Text>

View File

@@ -1,5 +1,5 @@
import profileLandingPageState from "@/app/admin/(dashboard)/_state/landing-page/profile"; import profileLandingPageState from "@/app/admin/(dashboard)/_state/landing-page/profile";
import { Center, Image, Paper, SimpleGrid } from "@mantine/core"; import { Center, Image, Paper, SimpleGrid, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
@@ -25,15 +25,21 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
<motion.div <motion.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
> >
<Image src={data.image?.link || ""} alt="icon" {data.image?.link ? (
fit="contain" <Image src={data.image.link} alt="icon"
sizes="100%" fit="contain"
loading="lazy" sizes="100%"
loading="lazy"
style={{ style={{
objectFit: "contain", objectFit: "contain",
objectPosition: "center" objectPosition: "center"
}} }}
/> />
) : (
<Text>
-
</Text>
)}
</motion.div> </motion.div>
</Center> </Center>
</Paper> </Paper>

View File

@@ -23,13 +23,17 @@ function ProfileView({ data }: ProfileViewProps) {
}} }}
px="xl" px="xl"
> >
{data.image?.link && ( {data.image?.link ? (
<Image <Image
src={data.image.link} src={data.image.link}
alt={data.name || "Profile image"} alt={data.name || "Profile image"}
sizes="100%" sizes="100%"
fit="contain" fit="contain"
/> />
): (
<Text>
-
</Text>
)} )}
<Box <Box
pos="absolute" pos="absolute"

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Flex, Image } from "@mantine/core"; import { ActionIcon, Flex, Image, Text } from "@mantine/core";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
@@ -20,7 +20,13 @@ function SosmedView({data} : {data : Prisma.MediaSosialGetPayload<{ include: { i
router.push(item.iconUrl || ""); router.push(item.iconUrl || "");
}} }}
> >
<Image src={item.image?.link || ""} alt="icon" loading="lazy" /> {item.image?.link ? (
<Image src={item.image.link} alt="icon" loading="lazy" />
) : (
<Text>
none
</Text>
)}
</ActionIcon> </ActionIcon>
); );
})} })}