Merge pull request #18 from bipproduction/nico/30-apr-25
Admin Dashboard Bagian Data Kesehatan
This commit is contained in:
@@ -62,7 +62,7 @@
|
|||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "2",
|
"recharts": "^2.15.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
"valtio": "^2.1.3",
|
"valtio": "^2.1.3",
|
||||||
"zod": "^3.24.3"
|
"zod": "^3.24.3"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ model AppMenuChild {
|
|||||||
AppMenu AppMenu? @relation(fields: [appMenuId], references: [id])
|
AppMenu AppMenu? @relation(fields: [appMenuId], references: [id])
|
||||||
appMenuId String?
|
appMenuId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= MENU DESA ========================================= //
|
// ========================================= MENU DESA ========================================= //
|
||||||
// ========================================= BERITA ========================================= //
|
// ========================================= BERITA ========================================= //
|
||||||
model Berita {
|
model Berita {
|
||||||
@@ -106,6 +107,7 @@ model Images {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
GalleryFoto GalleryFoto[]
|
GalleryFoto GalleryFoto[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= VIDEOS ========================================= //
|
// ========================================= VIDEOS ========================================= //
|
||||||
model Videos {
|
model Videos {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -117,7 +119,6 @@ model Videos {
|
|||||||
GalleryVideo GalleryVideo[]
|
GalleryVideo GalleryVideo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========================================= GALLERY ========================================= //
|
// ========================================= GALLERY ========================================= //
|
||||||
model GalleryFoto {
|
model GalleryFoto {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -132,14 +133,14 @@ model GalleryFoto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model GalleryVideo {
|
model GalleryVideo {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
video String
|
video 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)
|
||||||
videosId String? @unique
|
videosId String? @unique
|
||||||
videosGalleryVideo Videos? @relation(fields: [videosId], references: [id])
|
videosGalleryVideo Videos? @relation(fields: [videosId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +148,159 @@ model GalleryVideo {
|
|||||||
// ========================================= DATA KESEHATAN WARGA ========================================= //
|
// ========================================= DATA KESEHATAN WARGA ========================================= //
|
||||||
|
|
||||||
// ========================================= FASILITAS KESEHATAN ========================================= //
|
// ========================================= FASILITAS KESEHATAN ========================================= //
|
||||||
|
model FasilitasKesehatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
InformasiUmum InformasiUmum[]
|
||||||
|
LayananUnggulan LayananUnggulan[]
|
||||||
|
DokterdanTenagaMedis DokterdanTenagaMedis[]
|
||||||
|
FasilitasPendukung FasilitasPendukung[]
|
||||||
|
ProsedurPendaftaran ProsedurPendaftaran[]
|
||||||
|
TarifDanLayanan TarifDanLayanan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model InformasiUmum {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
fasilitas String
|
||||||
|
alamat String
|
||||||
|
jamOperasional String
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model LayananUnggulan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model DokterdanTenagaMedis {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
specialist String
|
||||||
|
jadwal String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model FasilitasPendukung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ProsedurPendaftaran {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model TarifDanLayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
layanan String
|
||||||
|
tarif String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= JADWAL KEGIATAN ========================================= //
|
||||||
|
model JadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model InformasiJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
tanggal String
|
||||||
|
waktu String
|
||||||
|
lokasi String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model DeskripsiJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
deskripsi String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model LayananJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model SyaratKetentuanJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model DokumenJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model PendaftaranJadwalKegiatan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
tanggal String
|
||||||
|
namaOrangtua String
|
||||||
|
nomor String
|
||||||
|
alamat String
|
||||||
|
catatan String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
|
||||||
model DataKematian_Kelahiran {
|
model DataKematian_Kelahiran {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
tahun String
|
tahun String
|
||||||
@@ -155,156 +309,17 @@ model DataKematian_Kelahiran {
|
|||||||
kelahiranKasar String
|
kelahiranKasar String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model FasilitasKesehatan {
|
// ========================================= GRAFIK KEPUASAN ========================================= //
|
||||||
id String @id @default(cuid())
|
model GrafikKepuasan {
|
||||||
name String
|
id Int @id @default(autoincrement())
|
||||||
|
label String
|
||||||
|
jumlah 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)
|
||||||
InformasiUmum InformasiUmum[]
|
|
||||||
LayananUnggulan LayananUnggulan[]
|
|
||||||
DokterdanTenagaMedis DokterdanTenagaMedis[]
|
|
||||||
FasilitasPendukung FasilitasPendukung[]
|
|
||||||
ProsedurPendaftaran ProsedurPendaftaran[]
|
|
||||||
TarifDanLayanan TarifDanLayanan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model InformasiUmum{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
fasilitas String
|
|
||||||
alamat String
|
|
||||||
jamOperasional String
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model LayananUnggulan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model DokterdanTenagaMedis{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
specialist String
|
|
||||||
jadwal String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model FasilitasPendukung{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model ProsedurPendaftaran{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model TarifDanLayanan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
layanan String
|
|
||||||
tarif String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================= JADWAL KEGIATAN ========================================= //
|
|
||||||
model JadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model InformasiJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
tanggal String
|
|
||||||
waktu String
|
|
||||||
lokasi String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model DeskripsiJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
deskripsi String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model LayananJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model SyaratKetentuanJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model DokumenJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
content String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model PendaftaranJadwalKegiatan{
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
tanggal DateTime
|
|
||||||
namaOrangtua String
|
|
||||||
nomor String
|
|
||||||
alamat String
|
|
||||||
catatan String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const templateGrafikKepuasan = z.object({
|
||||||
|
label: z.string().min(2, "Label harus diisi"),
|
||||||
|
jumlah: z.string().min(2, "Jumlah harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GrafikKepuasan = Prisma.GrafikKepuasanGetPayload<{
|
||||||
|
select: {
|
||||||
|
label: true;
|
||||||
|
jumlah: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const defaultForm: GrafikKepuasan = {
|
||||||
|
label: "",
|
||||||
|
jumlah: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const grafikkepuasan = proxy({
|
||||||
|
create: {
|
||||||
|
form: defaultForm,
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateGrafikKepuasan.safeParse(grafikkepuasan.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
grafikkepuasan.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(
|
||||||
|
grafikkepuasan.create.form
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
grafikkepuasan.create.form = {
|
||||||
|
label: "",
|
||||||
|
jumlah: ""
|
||||||
|
};
|
||||||
|
grafikkepuasan.findMany.load();
|
||||||
|
return toast.success("success create");
|
||||||
|
}
|
||||||
|
return toast.error("failed create");
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
grafikkepuasan.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.GrafikKepuasanGetPayload<{ omit: { isActive: true } }>[]
|
||||||
|
| null,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.kesehatan.grafikkepuasan[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
grafikkepuasan.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stategrafikKepuasan = proxy({
|
||||||
|
grafikkepuasan,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default stategrafikKepuasan;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const templatePersentase = z.object({
|
||||||
|
tahun: z.string().min(4, "Tahun harus diisi"),
|
||||||
|
kematianKasar: z.string().min(2, "Kematian kasar harus diisi"),
|
||||||
|
kelahiranKasar: z.string().min(2, "Kelahiran kasar harus diisi"),
|
||||||
|
kematianBayi: z.string().min(2, "Kematian bayi harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
|
||||||
|
select: {
|
||||||
|
tahun: true;
|
||||||
|
kematianKasar: true;
|
||||||
|
kelahiranKasar: true;
|
||||||
|
kematianBayi: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const defaultForm: Persentase = {
|
||||||
|
tahun: "",
|
||||||
|
kematianKasar: "",
|
||||||
|
kelahiranKasar: "",
|
||||||
|
kematianBayi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const persentasekelahiran = proxy({
|
||||||
|
create: {
|
||||||
|
form: defaultForm,
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templatePersentase.safeParse(persentasekelahiran.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
persentasekelahiran.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
|
||||||
|
"create"
|
||||||
|
].post(persentasekelahiran.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
persentasekelahiran.create.form = {
|
||||||
|
tahun: "",
|
||||||
|
kematianKasar: "",
|
||||||
|
kelahiranKasar: "",
|
||||||
|
kematianBayi: "",
|
||||||
|
};
|
||||||
|
persentasekelahiran.findMany.load();
|
||||||
|
return toast.success("success create");
|
||||||
|
}
|
||||||
|
return toast.error("failed create");
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
persentasekelahiran.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.DataKematian_KelahiranGetPayload<{ omit: { isActive: true } }>[]
|
||||||
|
| null,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
persentasekelahiran.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const statePersentase = proxy({
|
||||||
|
persentasekelahiran,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default statePersentase;
|
||||||
@@ -1,10 +1,72 @@
|
|||||||
import { Stack, Title } from '@mantine/core';
|
'use client'
|
||||||
import React from 'react';
|
import stategrafikKepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Stack, TextInput, Title } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Bar, Legend, RadialBarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function GrafikHasilKepuasan() {
|
function GrafikHasilKepuasan() {
|
||||||
|
const grafikkepuasan = useProxy(stategrafikKepuasan.grafikkepuasan)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [chartData, setChartData] = useState<any[]>([])
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
await grafikkepuasan.findMany.load();
|
||||||
|
if (grafikkepuasan.findMany.data && grafikkepuasan.findMany.data.length > 0) {
|
||||||
|
setChartData(grafikkepuasan.findMany.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack gap={"xs"}>
|
||||||
<Title order={3}>Grafik Hasil Kepuasan</Title>
|
<Title order={3}>Grafik Hasil Kepuasan</Title>
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
label="Label"
|
||||||
|
placeholder='Masukkan label yang diinginkan'
|
||||||
|
value={grafikkepuasan.create.form.label}
|
||||||
|
onChange={(val) => {
|
||||||
|
grafikkepuasan.create.form.label = val.currentTarget.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Jumlah Penderita Farangitis Akut"
|
||||||
|
type='number'
|
||||||
|
placeholder='Masukkan jumlah penderita farangitis akut'
|
||||||
|
value={grafikkepuasan.create.form.jumlah}
|
||||||
|
onChange={(val) => {
|
||||||
|
grafikkepuasan.create.form.jumlah = val.currentTarget.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Group>
|
||||||
|
<Button mt={10}
|
||||||
|
onClick={async () => {
|
||||||
|
await grafikkepuasan.create.create();
|
||||||
|
await grafikkepuasan.findMany.load();
|
||||||
|
if (grafikkepuasan.findMany.data) {
|
||||||
|
setChartData(grafikkepuasan.findMany.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
<Box h={400} w={{ base: "100%", md: "80%" }}>
|
||||||
|
<Title order={3}>Data Kelahiran & Kematian</Title>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<RadialBarChart
|
||||||
|
data={chartData}
|
||||||
|
>
|
||||||
|
<XAxis dataKey="label" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah" />
|
||||||
|
</RadialBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function JadwalKegiatan() {
|
|||||||
<SyaratDanKetentuan />
|
<SyaratDanKetentuan />
|
||||||
<DokumenYangDiperlukan />
|
<DokumenYangDiperlukan />
|
||||||
<Pendaftaran />
|
<Pendaftaran />
|
||||||
<Button onClick={submitAllForms}>
|
<Button mt={10} onClick={submitAllForms}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -84,13 +84,15 @@ function AllList() {
|
|||||||
allList.layanantersedia.findMany.load()
|
allList.layanantersedia.findMany.load()
|
||||||
allList.syaratketentuan.findMany.load()
|
allList.syaratketentuan.findMany.load()
|
||||||
allList.dokumenjadwalkegiatan.findMany.load()
|
allList.dokumenjadwalkegiatan.findMany.load()
|
||||||
})
|
allList.pendaftaranjadwal.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const isLoading = !allList.informasiKegiatan.findMany.data ||
|
const isLoading = !allList.informasiKegiatan.findMany.data ||
|
||||||
!allList.deskripsiKegiatan.findMany.data ||
|
!allList.deskripsiKegiatan.findMany.data ||
|
||||||
!allList.layanantersedia.findMany.data ||
|
!allList.layanantersedia.findMany.data ||
|
||||||
!allList.syaratketentuan.findMany.data ||
|
!allList.syaratketentuan.findMany.data ||
|
||||||
!allList.dokumenjadwalkegiatan.findMany.data
|
!allList.dokumenjadwalkegiatan.findMany.data ||
|
||||||
|
!allList.pendaftaranjadwal.findMany.data
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateJadwalKegiatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
|
import stateJadwalKegiatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
|
||||||
import { ActionIcon, Box, Text, Textarea, TextInput } from '@mantine/core';
|
import { Box, Text, Textarea, TextInput } from '@mantine/core';
|
||||||
import { DateInput } from '@mantine/dates';
|
|
||||||
import { IconCalendar } from '@tabler/icons-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function Pendaftaran() {
|
function Pendaftaran() {
|
||||||
const pendaftaran = useProxy(stateJadwalKegiatan.pendaftaranjadwal)
|
const pendaftaran = useProxy(stateJadwalKegiatan.pendaftaranjadwal)
|
||||||
const [dateInputOpened, setDateInputOpened] = useState(false);
|
|
||||||
const pickerControl = (
|
|
||||||
<ActionIcon onClick={() => setDateInputOpened(true)} variant="subtle" color="gray">
|
|
||||||
<IconCalendar size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatDate = (date: Date | null): string => { if (!date) return ""; return date.toISOString().split('T')[0]; }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -27,16 +16,11 @@ function Pendaftaran() {
|
|||||||
pendaftaran.create.form.name = val.target.value
|
pendaftaran.create.form.name = val.target.value
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<TextInput
|
||||||
clearable defaultValue={new Date()}
|
label='Tanggal'
|
||||||
styles={{label: {fontSize: '14px'}}}
|
placeholder='Masukkan tanggal'
|
||||||
label='Tanggal Lahir'
|
|
||||||
placeholder='dd/mm/yyyy'
|
|
||||||
value={pendaftaran.create.form.tanggal ? new Date(pendaftaran.create.form.tanggal) : null}
|
|
||||||
popoverProps={{opened: dateInputOpened, onChange: setDateInputOpened}}
|
|
||||||
rightSection={pickerControl}
|
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
pendaftaran.create.form.tanggal = formatDate(val);
|
pendaftaran.create.form.tanggal = val.target.value
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -1,10 +1,107 @@
|
|||||||
import { Stack, Title } from '@mantine/core';
|
'use client'
|
||||||
import React from 'react';
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import statePersentase from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
|
||||||
|
import { Box, Button, Stack, TextInput, Title } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function PersentaseDataKelahiranKematian() {
|
function PersentaseDataKelahiranKematian() {
|
||||||
|
const persentase = useProxy(statePersentase.persentasekelahiran);
|
||||||
|
const [chartData, setChartData] = useState<any[]>([]);
|
||||||
|
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
await persentase.findMany.load();
|
||||||
|
if (persentase.findMany.data && persentase.findMany.data.length > 0) {
|
||||||
|
setChartData(persentase.findMany.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Title order={3}>Persentase Data Kelahiran & Kematian</Title>
|
{/* Form Input */}
|
||||||
|
<Box>
|
||||||
|
<Title order={3}>Persentase Data Kelahiran & Kematian</Title>
|
||||||
|
<TextInput
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
label="Tahun"
|
||||||
|
type="number"
|
||||||
|
value={persentase.create.form.tahun}
|
||||||
|
placeholder="Masukkan tahun"
|
||||||
|
onChange={(val) => {
|
||||||
|
persentase.create.form.tahun = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
label="Kematian Kasar"
|
||||||
|
type="number"
|
||||||
|
value={persentase.create.form.kematianKasar}
|
||||||
|
placeholder="Masukkan kematian kasar"
|
||||||
|
onChange={(val) => {
|
||||||
|
persentase.create.form.kematianKasar = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
label="Kematian Bayi"
|
||||||
|
type="number"
|
||||||
|
value={persentase.create.form.kematianBayi}
|
||||||
|
placeholder="Masukkan kematian bayi"
|
||||||
|
onChange={(val) => {
|
||||||
|
persentase.create.form.kematianBayi = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
label="Kelahiran Kasar"
|
||||||
|
type="number"
|
||||||
|
value={persentase.create.form.kelahiranKasar}
|
||||||
|
placeholder="Masukkan kelahiran kasar"
|
||||||
|
onChange={(val) => {
|
||||||
|
persentase.create.form.kelahiranKasar = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
mt={10}
|
||||||
|
onClick={async () => {
|
||||||
|
await persentase.create.create();
|
||||||
|
await persentase.findMany.load();
|
||||||
|
if (persentase.findMany.data) {
|
||||||
|
setChartData(persentase.findMany.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Chart */}
|
||||||
|
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||||
|
<Title order={3}>Data Kelahiran & Kematian</Title>
|
||||||
|
{mounted && chartData.length > 0 && (
|
||||||
|
<ResponsiveContainer width="100%" aspect={2}>
|
||||||
|
<BarChart width={300} 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>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from '@mantine/core';
|
||||||
import ArtikelKesehatan from './_ui/artikel_kesehatan/page';
|
import ArtikelKesehatan from './_ui/artikel_kesehatan/page';
|
||||||
import FasilitasKesehatan from './_ui/fasilitas_kesehatan/page';
|
import FasilitasKesehatan from './_ui/fasilitas_kesehatan/page';
|
||||||
|
import GrafikHasilKepuasan from './_ui/grafik_hasil_kepuasan/page';
|
||||||
import JadwalKegiatan from './_ui/jadwal_kegiatan/page';
|
import JadwalKegiatan from './_ui/jadwal_kegiatan/page';
|
||||||
import PersentaseDataKelahiranKematian from './_ui/persentase_data_kelahiran_kematian/page';
|
import PersentaseDataKelahiranKematian from './_ui/persentase_data_kelahiran_kematian/page';
|
||||||
import GrafikHasilKepuasan from './_ui/grafik_hasil_kepuasan/page';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreate = Prisma.GrafikKepuasanGetPayload<{
|
||||||
|
select: {
|
||||||
|
label: true;
|
||||||
|
jumlah: true
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
export default async function grafikKepuasanCreate(context: Context) {
|
||||||
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
|
await prisma.grafikKepuasan.create({
|
||||||
|
data: {
|
||||||
|
label: body.label,
|
||||||
|
jumlah: body.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success create grafik kepuasan",
|
||||||
|
data: {
|
||||||
|
...body,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import prisma from "@/lib/prisma"
|
||||||
|
|
||||||
|
export default async function grafikKepuasanFindMany() {
|
||||||
|
const res = await prisma.grafikKepuasan.findMany()
|
||||||
|
return {
|
||||||
|
data: res
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import grafikKepuasanCreate from "./create";
|
||||||
|
import grafikKepuasanFindMany from "./find-many";
|
||||||
|
|
||||||
|
const GrafikKepuasan = new Elysia({
|
||||||
|
prefix: "/grafikkepuasan",
|
||||||
|
tags: ["Data Kesehatan/Grafik Kepuasan"]
|
||||||
|
})
|
||||||
|
.get("/find-many", grafikKepuasanFindMany)
|
||||||
|
.post("/create", grafikKepuasanCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
label: t.String(),
|
||||||
|
jumlah: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
export default GrafikKepuasan
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreate = Prisma.DataKematian_KelahiranGetPayload<{
|
||||||
|
select: {
|
||||||
|
tahun: true;
|
||||||
|
kematianKasar: true;
|
||||||
|
kematianBayi: true;
|
||||||
|
kelahiranKasar: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default async function persentaseKelahiranKematianCreate(context: Context) {
|
||||||
|
const body = context.body as FormCreate
|
||||||
|
|
||||||
|
await prisma.dataKematian_Kelahiran.create({
|
||||||
|
data: {
|
||||||
|
tahun: body.tahun,
|
||||||
|
kematianKasar: body.kematianKasar,
|
||||||
|
kematianBayi: body.kematianBayi,
|
||||||
|
kelahiranKasar: body.kelahiranKasar,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return{
|
||||||
|
success: true,
|
||||||
|
message: "Success create persentase kelahiran kematian",
|
||||||
|
data: {
|
||||||
|
...body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function persentaseKelahiranKematianFindMany() {
|
||||||
|
const res = await prisma.dataKematian_Kelahiran.findMany();
|
||||||
|
return {
|
||||||
|
data: res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import persentaseKelahiranKematianCreate from "./create";
|
||||||
|
import persentaseKelahiranKematianFindMany from "./find-many";
|
||||||
|
|
||||||
|
const PersentaseKelahiranKematian = new Elysia({
|
||||||
|
prefix: "/persentasekelahiran",
|
||||||
|
tags: ["Data Kesehatan/Persentase Kelahiran Kematian"],
|
||||||
|
})
|
||||||
|
.get("/find-many", persentaseKelahiranKematianFindMany)
|
||||||
|
.post("/create", persentaseKelahiranKematianCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
tahun: t.String(),
|
||||||
|
kematianKasar: t.String(),
|
||||||
|
kematianBayi: t.String(),
|
||||||
|
kelahiranKasar: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PersentaseKelahiranKematian;
|
||||||
@@ -11,6 +11,9 @@ import LayananTersedia from "./data_kesehatan_warga/jadwal_kegiatan/layanan_yang
|
|||||||
import SyaratKetentuan from "./data_kesehatan_warga/jadwal_kegiatan/syarat_dan_ketentuan";
|
import SyaratKetentuan from "./data_kesehatan_warga/jadwal_kegiatan/syarat_dan_ketentuan";
|
||||||
import DokumenDiperlukan from "./data_kesehatan_warga/jadwal_kegiatan/dokumen_yang_diperlukan";
|
import DokumenDiperlukan from "./data_kesehatan_warga/jadwal_kegiatan/dokumen_yang_diperlukan";
|
||||||
import PendaftaranJadwal from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
|
import PendaftaranJadwal from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
|
||||||
|
import PersentaseKelahiranKematian from "./data_kesehatan_warga/persentase_kelahiran_kematian";
|
||||||
|
import GrafikKepuasan from "./data_kesehatan_warga/grafik_kepuasan";
|
||||||
|
|
||||||
|
|
||||||
const Kesehatan = new Elysia({
|
const Kesehatan = new Elysia({
|
||||||
prefix: "/api/kesehatan",
|
prefix: "/api/kesehatan",
|
||||||
@@ -28,4 +31,6 @@ const Kesehatan = new Elysia({
|
|||||||
.use(SyaratKetentuan)
|
.use(SyaratKetentuan)
|
||||||
.use(DokumenDiperlukan)
|
.use(DokumenDiperlukan)
|
||||||
.use(PendaftaranJadwal)
|
.use(PendaftaranJadwal)
|
||||||
|
.use(PersentaseKelahiranKematian)
|
||||||
|
.use(GrafikKepuasan)
|
||||||
export default Kesehatan;
|
export default Kesehatan;
|
||||||
|
|||||||
300
src/app/percobaan/_lib/ClientRouter.txt
Normal file
300
src/app/percobaan/_lib/ClientRouter.txt
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
"use client";
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type RouterLeaf<T extends z.ZodType = z.ZodObject<{}>> = {
|
||||||
|
get: () => string;
|
||||||
|
query: (params: z.infer<T>) => string;
|
||||||
|
parse: (searchParams: URLSearchParams) => z.infer<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper type to convert dashes to camelCase
|
||||||
|
type DashToCamelCase<S extends string> = S extends `${infer F}-${infer R}`
|
||||||
|
? `${F}${Capitalize<DashToCamelCase<R>>}`
|
||||||
|
: S;
|
||||||
|
|
||||||
|
// Modified RouterPath to handle dash conversion
|
||||||
|
type RouterPath<
|
||||||
|
T extends z.ZodType = z.ZodObject<{}>,
|
||||||
|
Segments extends string[] = []
|
||||||
|
> = Segments extends [infer Head extends string, ...infer Tail extends string[]]
|
||||||
|
? { [K in DashToCamelCase<Head>]: RouterPath<T, Tail> }
|
||||||
|
: RouterLeaf<T>;
|
||||||
|
|
||||||
|
type RemoveLeadingSlash<S extends string> = S extends `/${infer Rest}`
|
||||||
|
? Rest
|
||||||
|
: S;
|
||||||
|
|
||||||
|
type SplitPath<S extends string> = S extends `${infer Head}/${infer Tail}`
|
||||||
|
? [Head, ...SplitPath<Tail>]
|
||||||
|
: S extends ""
|
||||||
|
? []
|
||||||
|
: [S];
|
||||||
|
|
||||||
|
type WibuRouterOptions = {
|
||||||
|
prefix?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class V2ClientRouter<Routes = {}> {
|
||||||
|
private tree: any = {};
|
||||||
|
private prefix: string = "";
|
||||||
|
private name: string = "";
|
||||||
|
private querySchemas: Map<string, z.ZodType> = new Map();
|
||||||
|
|
||||||
|
constructor(options?: WibuRouterOptions) {
|
||||||
|
if (options?.prefix) {
|
||||||
|
// Ensure prefix starts with / and doesn't end with /
|
||||||
|
this.prefix = options.prefix.startsWith("/")
|
||||||
|
? options.prefix
|
||||||
|
: `/${options.prefix}`;
|
||||||
|
|
||||||
|
if (this.prefix.endsWith("/")) {
|
||||||
|
this.prefix = this.prefix.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.name) {
|
||||||
|
this.name = options.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert dash-case to camelCase
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
add<
|
||||||
|
Path extends string,
|
||||||
|
NormalizedPath extends string = RemoveLeadingSlash<Path>,
|
||||||
|
Segments extends string[] = SplitPath<NormalizedPath>,
|
||||||
|
T extends z.ZodType = z.ZodObject<{}>
|
||||||
|
>(
|
||||||
|
path: Path,
|
||||||
|
schema?: { query: T }
|
||||||
|
): V2ClientRouter<
|
||||||
|
Routes &
|
||||||
|
(NormalizedPath extends ""
|
||||||
|
? RouterLeaf<T>
|
||||||
|
: {
|
||||||
|
[K in Segments[0] as DashToCamelCase<K>]: RouterPath<
|
||||||
|
T,
|
||||||
|
Segments extends [any, ...infer Rest] ? Rest : []
|
||||||
|
>;
|
||||||
|
})
|
||||||
|
> {
|
||||||
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
const fullPath = `${this.prefix}${normalizedPath}`;
|
||||||
|
const segments = normalizedPath.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
// Store the Zod schema for this path
|
||||||
|
if (schema) {
|
||||||
|
this.querySchemas.set(fullPath, schema.query);
|
||||||
|
} else {
|
||||||
|
// Default empty schema
|
||||||
|
this.querySchemas.set(fullPath, z.object({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuery = (params: any): string => {
|
||||||
|
if (!params || Object.keys(params).length === 0) return fullPath;
|
||||||
|
|
||||||
|
// Validate params against schema
|
||||||
|
const schema = this.querySchemas.get(fullPath);
|
||||||
|
if (schema) {
|
||||||
|
try {
|
||||||
|
schema.parse(params);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Query params validation failed:", error);
|
||||||
|
throw new Error("Invalid query parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = Object.entries(params)
|
||||||
|
.map(
|
||||||
|
([key, value]) =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
return `${fullPath}?${queryString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGet = () => fullPath;
|
||||||
|
|
||||||
|
const handleParse = (searchParams: URLSearchParams): any => {
|
||||||
|
const schema = this.querySchemas.get(fullPath);
|
||||||
|
if (!schema) return {};
|
||||||
|
|
||||||
|
// Convert URLSearchParams to object
|
||||||
|
const queryObject: Record<string, any> = {};
|
||||||
|
searchParams.forEach((value, key) => {
|
||||||
|
queryObject[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse through Zod schema
|
||||||
|
try {
|
||||||
|
return schema.parse(queryObject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse search params:", error);
|
||||||
|
// Return safe default values
|
||||||
|
const safeParsed = schema.safeParse(queryObject);
|
||||||
|
if (safeParsed.success) {
|
||||||
|
return safeParsed.data;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Special case for root path "/"
|
||||||
|
if (segments.length === 0) {
|
||||||
|
this.tree.get = handleGet;
|
||||||
|
this.tree.query = handleQuery;
|
||||||
|
this.tree.parse = handleParse;
|
||||||
|
} else {
|
||||||
|
let current = this.tree;
|
||||||
|
for (const segment of segments) {
|
||||||
|
// Use camelCase version for the property name
|
||||||
|
const camelSegment = this.toCamelCase(segment);
|
||||||
|
if (!current[camelSegment]) {
|
||||||
|
current[camelSegment] = {};
|
||||||
|
}
|
||||||
|
current = current[camelSegment];
|
||||||
|
}
|
||||||
|
|
||||||
|
current.get = handleGet;
|
||||||
|
current.query = handleQuery;
|
||||||
|
current.parse = handleParse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a method to incorporate another router's routes into this one
|
||||||
|
use<N extends string, ChildRoutes>(
|
||||||
|
name: N,
|
||||||
|
childRouter: V2ClientRouter<ChildRoutes>
|
||||||
|
): V2ClientRouter<Routes & Record<DashToCamelCase<N>, ChildRoutes>> {
|
||||||
|
const camelName = this.toCamelCase(name);
|
||||||
|
|
||||||
|
if (!this.tree[camelName]) {
|
||||||
|
this.tree[camelName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy query schemas from child router
|
||||||
|
childRouter.querySchemas.forEach((schema, path) => {
|
||||||
|
const newPath = `${this.prefix}/${name}${path.substring(
|
||||||
|
childRouter.prefix.length
|
||||||
|
)}`;
|
||||||
|
this.querySchemas.set(newPath, schema);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a deep copy of the child router's tree with updated paths
|
||||||
|
const updatePaths = (obj: any, childPrefix: string): any => {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
if (key === "get" && typeof obj[key] === "function") {
|
||||||
|
// Capture the original path from the child router
|
||||||
|
const originalPath = obj[key]();
|
||||||
|
// Create a new function that returns the combined path
|
||||||
|
result[key] = () => {
|
||||||
|
const newPath = `${this.prefix}/${name}${originalPath.substring(
|
||||||
|
childPrefix.length
|
||||||
|
)}`;
|
||||||
|
return newPath;
|
||||||
|
};
|
||||||
|
} else if (key === "query" && typeof obj[key] === "function") {
|
||||||
|
// Capture the child router's prefix for path adjustment
|
||||||
|
result[key] = (params: any) => {
|
||||||
|
// Get the original result without query params
|
||||||
|
const originalPathWithoutParams = obj["get"]();
|
||||||
|
// Create the proper path with our parent prefix
|
||||||
|
const newBasePath = `${
|
||||||
|
this.prefix
|
||||||
|
}/${name}${originalPathWithoutParams.substring(
|
||||||
|
childPrefix.length
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
// Add query params if any
|
||||||
|
if (!params || Object.keys(params).length === 0) return newBasePath;
|
||||||
|
|
||||||
|
// Validate params against schema
|
||||||
|
const newPath = `${
|
||||||
|
this.prefix
|
||||||
|
}/${name}${originalPathWithoutParams.substring(
|
||||||
|
childPrefix.length
|
||||||
|
)}`;
|
||||||
|
const schema = this.querySchemas.get(newPath);
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
try {
|
||||||
|
schema.parse(params);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Query params validation failed:", error);
|
||||||
|
throw new Error("Invalid query parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = Object.entries(params)
|
||||||
|
.map(
|
||||||
|
([k, v]) =>
|
||||||
|
`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
return `${newBasePath}?${queryString}`;
|
||||||
|
};
|
||||||
|
} else if (key === "parse" && typeof obj[key] === "function") {
|
||||||
|
result[key] = (searchParams: URLSearchParams) => {
|
||||||
|
const originalPath = obj["get"]();
|
||||||
|
const newPath = `${this.prefix}/${name}${originalPath.substring(
|
||||||
|
childPrefix.length
|
||||||
|
)}`;
|
||||||
|
const schema = this.querySchemas.get(newPath);
|
||||||
|
|
||||||
|
if (!schema) return {};
|
||||||
|
|
||||||
|
// Convert URLSearchParams to object
|
||||||
|
const queryObject: Record<string, any> = {};
|
||||||
|
searchParams.forEach((value, key) => {
|
||||||
|
queryObject[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse through Zod schema
|
||||||
|
try {
|
||||||
|
return schema.parse(queryObject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse search params:", error);
|
||||||
|
// Return safe default values
|
||||||
|
const safeParsed = schema.safeParse(queryObject);
|
||||||
|
if (safeParsed.success) {
|
||||||
|
return safeParsed.data;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||||
|
result[key] = updatePaths(obj[key], childPrefix);
|
||||||
|
} else {
|
||||||
|
result[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy the child router's tree into this router
|
||||||
|
this.tree[camelName] = updatePaths(
|
||||||
|
(childRouter as any).tree,
|
||||||
|
childRouter.prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
return this as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow access to the tree with strong typing
|
||||||
|
get routes(): Routes {
|
||||||
|
return this.tree as Routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default V2ClientRouter;
|
||||||
20
src/app/percobaan/_router/router.txt
Normal file
20
src/app/percobaan/_router/router.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import V2ClientRouter from "../_lib/ClientRouter";
|
||||||
|
|
||||||
|
const dashboard = new V2ClientRouter({
|
||||||
|
prefix: "/dashboard",
|
||||||
|
name: "dashboard",
|
||||||
|
})
|
||||||
|
.add("/", {
|
||||||
|
query: z.object({
|
||||||
|
page: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.add("/berita");
|
||||||
|
|
||||||
|
const router = new V2ClientRouter({
|
||||||
|
prefix: "/percobaan",
|
||||||
|
name: "percobaan",
|
||||||
|
}).use("dashboard", dashboard);
|
||||||
|
|
||||||
|
export default router;
|
||||||
11
src/app/percobaan/dashboard/berita/page.txt
Normal file
11
src/app/percobaan/dashboard/berita/page.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
berita
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
33
src/app/percobaan/dashboard/page.txt
Normal file
33
src/app/percobaan/dashboard/page.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client'
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import router from '../_router/router';
|
||||||
|
import { Box } from '@mantine/core';
|
||||||
|
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
const { page } = router.routes.dashboard.parse(useSearchParams())
|
||||||
|
switch (page) {
|
||||||
|
case "1":
|
||||||
|
return <Page1 />
|
||||||
|
case "2":
|
||||||
|
return <Page2 />
|
||||||
|
case "3":
|
||||||
|
return <Page3 />
|
||||||
|
default:
|
||||||
|
return <Page1 />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page1 = () => {
|
||||||
|
return <Box h={200} bg="red">Page 1</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page2 = () => {
|
||||||
|
return <Box h={200} bg="blue">Page 2</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page3 = () => {
|
||||||
|
return <Box h={200} bg="green">Page 3</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
18
src/app/percobaan/page2.txt
Normal file
18
src/app/percobaan/page2.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use client'
|
||||||
|
import { Button, Group, Stack } from "@mantine/core"
|
||||||
|
import { Link } from "next-view-transitions"
|
||||||
|
import router from "./_router/router"
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <Group>
|
||||||
|
<Stack>
|
||||||
|
{[1, 2, 3].map((v) => (<Button component={Link} href={router.routes.dashboard.query({
|
||||||
|
page: v.toString(),
|
||||||
|
})} key={v}>ke dashboard {v}</Button>))}
|
||||||
|
|
||||||
|
<Button component={Link} href={router.routes.dashboard.berita.get()}>berita</Button>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
Reference in New Issue
Block a user