Menambahkan menu dokter dan tenaga medis, admin bisa create, edit, delet dokter

Menambahkan menu tarif dan layanan, admin bisa create, edit, delete tarif dan layanan
Dibagian fasilitas kesehatan admin bisa multiselect bagian dokter dan tarif layanan
Di tampilan user juga sudah disesuaikan dengan datanya bisa muncul lebih dari 1 dokter dan 1 tarif layanan
This commit is contained in:
2025-12-03 17:24:03 +08:00
parent a4069d3cba
commit 7bb17ddf22
33 changed files with 2177 additions and 652 deletions

View File

@@ -793,14 +793,12 @@ model FasilitasKesehatan {
informasiUmumId String informasiUmumId String
layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id]) layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
layananUnggulanId String layananUnggulanId String
dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id]) dokterdantenagamedis DokterdanTenagaMedis[] @relation("Dokter")
dokterdanTenagaMedisId String
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id]) fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
fasilitasPendukungId String fasilitasPendukungId String
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id]) prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
prosedurPendaftaranId String prosedurPendaftaranId String
tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id]) tarifdanlayanan TarifDanLayanan[] @relation("Tarif")
tarifDanLayananId String
} }
model InformasiUmum { model InformasiUmum {
@@ -830,11 +828,16 @@ model DokterdanTenagaMedis {
name String name String
specialist String specialist String
jadwal String jadwal String
jadwalLibur String
jamBukaOperasional String
jamTutupOperasional String
jamBukaLibur String
jamTutupLibur 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)
FasilitasKesehatan FasilitasKesehatan[] FasilitasKesehatan FasilitasKesehatan[] @relation("Dokter")
} }
model FasilitasPendukung { model FasilitasPendukung {
@@ -865,7 +868,7 @@ model TarifDanLayanan {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
FasilitasKesehatan FasilitasKesehatan[] FasilitasKesehatan FasilitasKesehatan[] @relation("Tarif")
} }
// ========================================= JADWAL KEGIATAN ========================================= // // ========================================= JADWAL KEGIATAN ========================================= //

View File

@@ -9,29 +9,30 @@ import { z } from "zod";
// 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"),
informasiUmum: z.object({ informasiUmum: z.object({
fasilitas: z.string().min(1, "Fasilitas harus diisi"), fasilitas: z.string().min(1),
alamat: z.string().min(1, "Alamat harus diisi"), alamat: z.string().min(1),
jamOperasional: z.string().min(1, "Jam operasional harus diisi"), jamOperasional: z.string().min(1),
}), }),
layananUnggulan: z.object({ layananUnggulan: z.object({
content: z.string().min(1, "Layanan unggulan harus diisi"), content: z.string().min(1),
}),
dokterdanTenagaMedis: z.object({
name: z.string().min(1, "Nama dokter harus diisi"),
specialist: z.string().min(1, "Spesialis harus diisi"),
jadwal: z.string().min(1, "Jadwal harus diisi"),
}), }),
// NOW ARRAY OF STRING (ID)
dokterdanTenagaMedis: z.array(z.string()).min(1, "Minimal pilih 1 dokter"),
fasilitasPendukung: z.object({ fasilitasPendukung: z.object({
content: z.string().min(1, "Fasilitas pendukung harus diisi"), content: z.string().min(1),
}), }),
prosedurPendaftaran: z.object({ prosedurPendaftaran: z.object({
content: z.string().min(1, "Prosedur pendaftaran harus diisi"), content: z.string().min(1),
}),
tarifDanLayanan: z.object({
layanan: z.string().min(1, "Layanan harus diisi"),
tarif: z.string().min(1, "Tarif harus diisi"),
}), }),
// NOW ARRAY OF STRING (ID)
tarifDanLayanan: z.array(z.string()).min(1, "Minimal pilih 1 tarif"),
}); });
// Default form kosong // Default form kosong
@@ -45,21 +46,34 @@ const defaultForm = {
layananUnggulan: { layananUnggulan: {
content: "", content: "",
}, },
dokterdanTenagaMedis: {
name: "", dokterdanTenagaMedis: [] as string[], // ← array kosong
specialist: "", tarifDanLayanan: [] as string[], // ← array kosong
jadwal: "",
},
fasilitasPendukung: { fasilitasPendukung: {
content: "", content: "",
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: "", content: "",
}, },
tarifDanLayanan: { };
layanan: "",
tarif: "", type DokterItem = {
}, id: string;
name: string;
specialist: string;
jadwal: string;
jadwalLibur: string;
jamBukaOperasional: string;
jamTutupOperasional: string;
jamBukaLibur: string;
jamTutupLibur: string;
};
type TarifItem = {
id: string;
layanan: string;
tarif: string;
}; };
const fasilitasKesehatan = proxy({ const fasilitasKesehatan = proxy({
@@ -186,33 +200,26 @@ const fasilitasKesehatan = proxy({
const result = await res.json(); const result = await res.json();
const data = result.data; const data = result.data;
this.id = data.id;
fasilitasKesehatan.edit.id = data.id; this.form = {
fasilitasKesehatan.edit.form = {
name: data.name, name: data.name,
informasiUmum: { informasiUmum: {
fasilitas: data.informasiumum.fasilitas, fasilitas: data.informasiumum.fasilitas,
alamat: data.informasiumum.alamat, alamat: data.informasiumum.alamat,
jamOperasional: data.informasiumum.jamOperasional, jamOperasional: data.informasiumum.jamOperasional,
}, },
layananUnggulan: {
content: data.layananunggulan.content,
},
dokterdanTenagaMedis: {
name: data.dokterdantenagamedis.name,
specialist: data.dokterdantenagamedis.specialist,
jadwal: data.dokterdantenagamedis.jadwal,
},
fasilitasPendukung: { fasilitasPendukung: {
content: data.fasilitaspendukung.content, content: data.fasilitaspendukung.content,
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: data.prosedurpendaftaran.content, content: data.prosedurpendaftaran.content,
}, },
tarifDanLayanan: { // map relasi -> array of IDs
layanan: data.tarifdanlayanan.layanan, layananUnggulan: {
tarif: data.tarifdanlayanan.tarif, content: data.layananunggulan.content,
}, },
dokterdanTenagaMedis: data.dokterdantenagamedis?.map((v: DokterItem) => v.id) ?? [],
tarifDanLayanan: data.tarifdanlayanan?.map((v: TarifItem) => v.id) ?? [],
}; };
}, },
async submit() { async submit() {
@@ -238,22 +245,15 @@ const fasilitasKesehatan = proxy({
layananUnggulan: { layananUnggulan: {
content: fasilitasKesehatan.edit.form.layananUnggulan.content, content: fasilitasKesehatan.edit.form.layananUnggulan.content,
}, },
dokterdanTenagaMedis: { dokterdanTenagaMedis:
name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name, fasilitasKesehatan.edit.form.dokterdanTenagaMedis,
specialist:
fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
},
fasilitasPendukung: { fasilitasPendukung: {
content: fasilitasKesehatan.edit.form.fasilitasPendukung.content, content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content, content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
}, },
tarifDanLayanan: { tarifDanLayanan: fasilitasKesehatan.edit.form.tarifDanLayanan,
layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
},
}; };
const res = await fetch( const res = await fetch(
@@ -320,12 +320,26 @@ const templateDokterForm = z.object({
name: z.string().min(1, "Nama tidak boleh kosong"), name: z.string().min(1, "Nama tidak boleh kosong"),
specialist: z.string().min(1, "Spesialis tidak boleh kosong"), specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"), jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
jadwalLibur: z.string().min(1, "Jadwal libur tidak boleh kosong"),
jamBukaOperasional: z
.string()
.min(1, "Jam buka operasional tidak boleh kosong"),
jamTutupOperasional: z
.string()
.min(1, "Jam tutup operasional tidak boleh kosong"),
jamBukaLibur: z.string().min(1, "Jam buka libur tidak boleh kosong"),
jamTutupLibur: z.string().min(1, "Jam tutup libur tidak boleh kosong"),
}); });
const defaultDokterForm = { const defaultDokterForm = {
name: "", name: "",
specialist: "", specialist: "",
jadwal: "", jadwal: "",
jadwalLibur: "",
jamBukaOperasional: "",
jamTutupOperasional: "",
jamBukaLibur: "",
jamTutupLibur: "",
}; };
const dokter = proxy({ const dokter = proxy({
@@ -463,6 +477,11 @@ const dokter = proxy({
name: data.name, name: data.name,
specialist: data.specialist, specialist: data.specialist,
jadwal: data.jadwal, jadwal: data.jadwal,
jadwalLibur: data.jadwalLibur,
jamBukaOperasional: data.jamBukaOperasional,
jamTutupOperasional: data.jamTutupOperasional,
jamBukaLibur: data.jamBukaLibur,
jamTutupLibur: data.jamTutupLibur,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -487,6 +506,11 @@ const dokter = proxy({
name: this.form.name, name: this.form.name,
specialist: this.form.specialist, specialist: this.form.specialist,
jadwal: this.form.jadwal, jadwal: this.form.jadwal,
jadwalLibur: this.form.jadwalLibur,
jamBukaOperasional: this.form.jamBukaOperasional,
jamTutupOperasional: this.form.jamTutupOperasional,
jamBukaLibur: this.form.jamBukaLibur,
jamTutupLibur: this.form.jamTutupLibur,
}; };
const cek = templateDokterForm.safeParse(formData); const cek = templateDokterForm.safeParse(formData);
@@ -567,9 +591,255 @@ const dokter = proxy({
}, },
}); });
const templateTarifForm = z.object({
tarif: z.string().min(1, "Tarif tidak boleh kosong"),
layanan: z.string().min(1, "Layanan tidak boleh kosong"),
});
const defaultTarifForm = {
tarif: "",
layanan: "",
};
const tarif = proxy({
create: {
form: defaultTarifForm,
loading: false,
async create() {
const cek = templateTarifForm.safeParse(tarif.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
tarif.create.loading = true;
const res = await ApiFetch.api.kesehatan.tarifdanlayanan["create"].post(
tarif.create.form
);
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Sukses menambahkan");
tarif.create.form = { ...defaultTarifForm };
tarif.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
tarif.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.TarifDanLayananGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
tarif.findMany.loading = true; // ✅ Akses langsung via nama path
tarif.findMany.page = page;
tarif.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.tarifdanlayanan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
tarif.findMany.data = res.data.data ?? [];
tarif.findMany.totalPages = res.data.totalPages ?? 1;
} else {
tarif.findMany.data = [];
tarif.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch tarif dan layanan paginated:", err);
tarif.findMany.data = [];
tarif.findMany.totalPages = 1;
} finally {
tarif.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.TarifDanLayananGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/tarifdanlayanan/${id}`);
if (res.ok) {
const data = await res.json();
tarif.findUnique.data = data.data ?? null;
} else {
console.error(
"Failed to fetch tarif dan layanan",
res.statusText
);
tarif.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching tarif dan layanan", error);
tarif.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultTarifForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/tarifdanlayanan/${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 = {
tarif: data.tarif,
layanan: data.layanan
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading tarif dan layanan:", 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 = {
tarif: this.form.tarif,
layanan: this.form.layanan
};
const cek = templateTarifForm.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v: any) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/tarifdanlayanan/${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 tarif.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data tarif dan layanan");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
tarif.delete.loading = true;
const response = await fetch(
`/api/kesehatan/tarifdanlayanan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "tarif dan layanan berhasil dihapus"
);
await tarif.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus tarif dan layanan"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus tarif dan layanan");
} finally {
tarif.delete.loading = false;
}
},
},
});
const fasilitasKesehatanState = proxy({ const fasilitasKesehatanState = proxy({
fasilitasKesehatan, fasilitasKesehatan,
dokter, dokter,
tarif
}); });
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'; 'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -10,19 +8,22 @@ import {
Button, Button,
Group, Group,
Loader, Loader,
MultiSelect,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, 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';
interface FasilitasKesehatanFormBase { // Tipe form yang SESUAI dengan logika relasi (array ID)
interface EditFasilitasKesehatanForm {
name: string; name: string;
informasiUmum: { informasiUmum: {
fasilitas: string; fasilitas: string;
@@ -30,128 +31,92 @@ interface FasilitasKesehatanFormBase {
jamOperasional: string; jamOperasional: string;
}; };
layananUnggulan: { content: string }; layananUnggulan: { content: string };
dokterdanTenagaMedis: { dokterdanTenagaMedis: string[]; // ← ARRAY ID
name: string;
specialist: string;
jadwal: string;
};
fasilitasPendukung: { content: string }; fasilitasPendukung: { content: string };
prosedurPendaftaran: { content: string }; prosedurPendaftaran: { content: string };
tarifDanLayanan: { tarifDanLayanan: string[]; // ← ARRAY ID
layanan: string;
tarif: string;
};
} }
function EditFasilitasKesehatan() { function EditFasilitasKesehatan() {
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const dokterState = useProxy(fasilitasKesehatanState.dokter);
const tarifState = useProxy(fasilitasKesehatanState.tarif);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams<{ id: string }>();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FasilitasKesehatanFormBase>({ const [formData, setFormData] = useState<EditFasilitasKesehatanForm>({
name: '', name: '',
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' }, informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
layananUnggulan: { content: '' }, layananUnggulan: { content: '' },
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' }, dokterdanTenagaMedis: [],
fasilitasPendukung: { content: '' }, fasilitasPendukung: { content: '' },
prosedurPendaftaran: { content: '' }, prosedurPendaftaran: { content: '' },
tarifDanLayanan: { layanan: '', tarif: '' }, tarifDanLayanan: [],
}); });
const [originalData, setOriginalData] = useState<FasilitasKesehatanFormBase>({ // Load data fasilitas & daftar dokter/tarif
name: '', useShallowEffect(() => {
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' }, const loadAll = async () => {
layananUnggulan: { content: '' }, const id = params?.id;
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
fasilitasPendukung: { content: '' },
prosedurPendaftaran: { content: '' },
tarifDanLayanan: { layanan: '', tarif: '' },
});
// Helper untuk update nested state
const updateForm = <K extends keyof FasilitasKesehatanFormBase>(
key: K,
value: FasilitasKesehatanFormBase[K]
) => setFormData(prev => ({ ...prev, [key]: value }));
const updateNested = <
K extends keyof FasilitasKesehatanFormBase,
N extends keyof FasilitasKesehatanFormBase[K]
>(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) =>
setFormData(prev => ({
...prev,
[key]: { ...prev[key] as object, [nestedKey]: value },
}));
const deepClone = (obj: any): any => {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.warn('Gagal deep clone dengan JSON fallback:', error);
return obj; // fallback (berisiko shared reference)
}
};
// Load data
useEffect(() => {
const load = async () => {
const id = params?.id as string;
if (!id) return; if (!id) return;
try { // Load dokter & tarif (untuk opsi MultiSelect)
await Promise.all([
dokterState.findMany.load(),
tarifState.findMany.load(),
]);
// Load data fasilitas
await state.edit.load(id); await state.edit.load(id);
const loadedData = state.edit.form; const loaded = state.edit.form;
if (loaded) {
if (!loadedData) { setFormData({
toast.error('Data tidak ditemukan'); name: loaded.name,
return; informasiUmum: loaded.informasiUmum,
} layananUnggulan: loaded.layananUnggulan,
dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [],
// Gunakan JSON fallback untuk deep clone fasilitasPendukung: loaded.fasilitasPendukung,
const clonedData = deepClone(loadedData) as FasilitasKesehatanFormBase; prosedurPendaftaran: loaded.prosedurPendaftaran,
tarifDanLayanan: loaded.tarifDanLayanan || [],
setFormData(clonedData); });
setOriginalData(clonedData);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data fasilitas kesehatan');
} }
}; };
load(); loadAll();
}, [params?.id]); }, [params?.id]);
const handleResetForm = () => { const updateForm = <K extends keyof EditFasilitasKesehatanForm>(
setFormData({ field: K,
name: originalData.name, value: EditFasilitasKesehatanForm[K]
informasiUmum: ) => {
{ setFormData(prev => ({ ...prev, [field]: value }));
fasilitas: originalData.informasiUmum.fasilitas,
alamat: originalData.informasiUmum.alamat,
jamOperasional: originalData.informasiUmum.jamOperasional
},
layananUnggulan: { content: originalData.layananUnggulan.content },
dokterdanTenagaMedis: {
name: originalData.dokterdanTenagaMedis.name,
specialist: originalData.dokterdanTenagaMedis.specialist,
jadwal: originalData.dokterdanTenagaMedis.jadwal
},
fasilitasPendukung: { content: originalData.fasilitasPendukung.content },
prosedurPendaftaran: { content: originalData.prosedurPendaftaran.content },
tarifDanLayanan: {
layanan: originalData.tarifDanLayanan.layanan,
tarif: originalData.tarifDanLayanan.tarif
},
});
toast.info("Form dikembalikan ke data awal");
}; };
// Submit const handleReset = () => {
const handleSubmit = async () => { const loaded = state.edit.form;
if (loaded) {
setFormData({
name: loaded.name,
informasiUmum: loaded.informasiUmum,
layananUnggulan: loaded.layananUnggulan,
dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [],
fasilitasPendukung: loaded.fasilitasPendukung,
prosedurPendaftaran: loaded.prosedurPendaftaran,
tarifDanLayanan: loaded.tarifDanLayanan || [],
});
toast.info('Form dikembalikan ke data awal');
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try { try {
setIsSubmitting(true); setIsSubmitting(true);
state.edit.form = { ...state.edit.form, ...formData };
// Update state Valtio
state.edit.form = { ...formData };
const success = await state.edit.submit(); const success = await state.edit.submit();
if (success) { if (success) {
toast.success('Fasilitas kesehatan berhasil diperbarui!'); toast.success('Fasilitas kesehatan berhasil diperbarui!');
@@ -159,14 +124,14 @@ function EditFasilitasKesehatan() {
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan'); toast.error('Gagal memperbarui data');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
@@ -189,7 +154,7 @@ function EditFasilitasKesehatan() {
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
label="Nama Fasilitas Kesehatan" label="Nama Fasilitas Kesehatan"
placeholder="Masukkan nama fasilitas kesehatan" placeholder="Masukkan nama"
value={formData.name} value={formData.name}
onChange={(e) => updateForm('name', e.target.value)} onChange={(e) => updateForm('name', e.target.value)}
required required
@@ -197,118 +162,108 @@ function EditFasilitasKesehatan() {
{/* Informasi Umum */} {/* Informasi Umum */}
<Box> <Box>
<Text fw="bold" mb={5}> <Text fw="bold" mb={5}>Informasi Umum</Text>
Informasi Umum
</Text>
<TextInput <TextInput
label="Fasilitas" label="Fasilitas"
value={formData.informasiUmum.fasilitas} value={formData.informasiUmum.fasilitas}
onChange={(e) => updateNested('informasiUmum', 'fasilitas', e.target.value)} onChange={(e) =>
updateForm('informasiUmum', {
...formData.informasiUmum,
fasilitas: e.target.value,
})
}
/> />
<TextInput <TextInput
label="Alamat" label="Alamat"
value={formData.informasiUmum.alamat} value={formData.informasiUmum.alamat}
onChange={(e) => updateNested('informasiUmum', 'alamat', e.target.value)} onChange={(e) =>
updateForm('informasiUmum', {
...formData.informasiUmum,
alamat: e.target.value,
})
}
/> />
<TextInput <TextInput
label="Jam Operasional" label="Jam Operasional"
value={formData.informasiUmum.jamOperasional} value={formData.informasiUmum.jamOperasional}
onChange={(e) => updateNested('informasiUmum', 'jamOperasional', e.target.value)} onChange={(e) =>
updateForm('informasiUmum', {
...formData.informasiUmum,
jamOperasional: e.target.value,
})
}
/> />
</Box> </Box>
{/* Layanan Unggulan */} {/* Layanan Unggulan */}
<Box> <Box>
<Text fw="bold" mb={5}> <Text fw="bold" mb={5}>Layanan Unggulan</Text>
Layanan Unggulan
</Text>
<EditEditor <EditEditor
value={formData.layananUnggulan.content} value={formData.layananUnggulan.content}
onChange={(v) => updateNested('layananUnggulan', 'content', v)} onChange={(v) => updateForm('layananUnggulan', { content: v })}
/> />
</Box> </Box>
{/* Dokter dan Tenaga Medis */} {/* Dokter & Tenaga Medis — MultiSelect */}
<Box> <MultiSelect
<Text fw="bold" mb={5}> label="Dokter & Tenaga Medis"
Dokter dan Tenaga Medis placeholder="Pilih dokter/tenaga medis"
</Text> data={
<TextInput dokterState.findMany.data?.map((d) => ({
label="Nama Dokter" value: d.id,
value={formData.dokterdanTenagaMedis.name} label: `${d.name} (${d.specialist})`,
onChange={(e) => updateNested('dokterdanTenagaMedis', 'name', e.target.value)} })) || []
/>
<TextInput
label="Specialist"
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) =>
updateNested('dokterdanTenagaMedis', 'specialist', e.target.value)
} }
value={formData.dokterdanTenagaMedis}
onChange={(val) => updateForm('dokterdanTenagaMedis', val)}
searchable
clearable
required
/> />
<TextInput
label="Jadwal"
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) => updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)}
/>
</Box>
{/* Fasilitas Pendukung */} {/* Fasilitas Pendukung */}
<Box> <Box>
<Text fw="bold" mb={5}> <Text fw="bold" mb={5}>Fasilitas Pendukung</Text>
Fasilitas Pendukung
</Text>
<EditEditor <EditEditor
value={formData.fasilitasPendukung.content} value={formData.fasilitasPendukung.content}
onChange={(v) => updateNested('fasilitasPendukung', 'content', v)} onChange={(v) => updateForm('fasilitasPendukung', { content: v })}
/> />
</Box> </Box>
{/* Prosedur Pendaftaran */} {/* Prosedur Pendaftaran */}
<Box> <Box>
<Text fw="bold" mb={5}> <Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
Prosedur Pendaftaran
</Text>
<EditEditor <EditEditor
value={formData.prosedurPendaftaran.content} value={formData.prosedurPendaftaran.content}
onChange={(v) => updateNested('prosedurPendaftaran', 'content', v)} onChange={(v) => updateForm('prosedurPendaftaran', { content: v })}
/> />
</Box> </Box>
{/* Tarif dan Layanan */} {/* Tarif & Layanan — MultiSelect */}
<Box> <MultiSelect
<Text fw="bold" mb={5}> label="Tarif & Layanan"
Tarif dan Layanan placeholder="Pilih layanan"
</Text> data={
<TextInput tarifState.findMany.data?.map((t) => ({
label="Tarif" value: t.id,
value={formData.tarifDanLayanan.tarif} label: `${t.layanan} - ${t.tarif}`,
onChange={(e) => updateNested('tarifDanLayanan', 'tarif', e.target.value)} })) || []
}
value={formData.tarifDanLayanan}
onChange={(val) => updateForm('tarifDanLayanan', val)}
searchable
clearable
required
/> />
<TextInput
label="Layanan"
value={formData.tarifDanLayanan.layanan}
onChange={(e) => updateNested('tarifDanLayanan', 'layanan', e.target.value)}
/>
</Box>
{/* Tombol Simpan */} {/* Aksi */}
<Group justify="right"> <Group justify="right">
{/* Tombol Batal */} <Button variant="outline" color="gray" radius="md" onClick={handleReset}>
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal Batal
</Button> </Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} type="submit"
radius="md" radius="md"
size="md"
style={{ style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff', color: '#fff',

View File

@@ -9,6 +9,12 @@ import {
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text Text
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
@@ -108,21 +114,79 @@ function DetailFasilitasKesehatan() {
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Dokter & Tenaga Medis</Text> <Text fz="lg" fw="bold" mb="sm">Dokter & Tenaga Medis</Text>
<Text fz="md" fw="bold">Nama</Text> {data.dokterdantenagamedis && data.dokterdantenagamedis.length > 0 ? (
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.name || '-'}</Text> <Box style={{ overflowX: 'auto', width: '100%' }}>
<Text fz="md" fw="bold">Spesialis</Text> <Table striped highlightOnHover withTableBorder>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.specialist || '-'}</Text> <TableThead>
<Text fz="md" fw="bold">Jadwal</Text> <TableTr>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.jadwal || '-'}</Text> <TableTh style={{ whiteSpace: 'nowrap' }}>Nama</TableTh>
<TableTh style={{ whiteSpace: 'nowrap' }}>Spesialis</TableTh>
<TableTh style={{ whiteSpace: 'nowrap' }}>Jadwal</TableTh>
<TableTh style={{ whiteSpace: 'nowrap' }}>Jam Operasional</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.dokterdantenagamedis.map((dokter) => (
<TableTr key={dokter.id}>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{dokter.name || '-'}
</TableTd>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{dokter.specialist || '-'}
</TableTd>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{dokter.jadwal || '-'}
</TableTd>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{dokter.jamBukaOperasional} {dokter.jamTutupOperasional}
{dokter.jadwalLibur && (
<>
<br />
<Text span c="dimmed" fz="xs">
Libur: {dokter.jadwalLibur} ({dokter.jamBukaLibur}{dokter.jamTutupLibur})
</Text>
</>
)}
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
) : (
<Text c="dimmed">Tidak ada dokter atau tenaga medis terdaftar.</Text>
)}
</Box> </Box>
<Box> <Box mt="xl">
<Text fz="lg" fw="bold">Tarif & Layanan</Text> <Text fz="lg" fw="bold" mb="sm">Tarif & Layanan</Text>
<Text fz="md" fw="bold">Layanan</Text> {data.tarifdanlayanan && data.tarifdanlayanan.length > 0 ? (
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.layanan || '-'}</Text> <Box style={{ overflowX: 'auto', width: '100%' }}>
<Text fz="md" fw="bold">Tarif</Text> <Table striped highlightOnHover withTableBorder>
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.tarif || '-'}</Text> <TableThead>
<TableTr>
<TableTh style={{ whiteSpace: 'nowrap' }}>Layanan</TableTh>
<TableTh style={{ whiteSpace: 'nowrap' }}>Tarif</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.tarifdanlayanan.map((tarif) => (
<TableTr key={tarif.id}>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{tarif.layanan || '-'}
</TableTd>
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
{tarif.tarif || '-'}
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
) : (
<Text c="dimmed">Tidak ada tarif atau layanan terdaftar.</Text>
)}
</Box> </Box>
{/* Aksi */} {/* Aksi */}

View File

@@ -7,19 +7,20 @@ import {
Button, Button,
Group, Group,
Loader, Loader,
MultiSelect,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } 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';
function CreateFasilitasKesehatan() { function CreateFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter(); const router = useRouter();
@@ -34,23 +35,16 @@ function CreateFasilitasKesehatan() {
jamOperasional: '', jamOperasional: '',
}, },
layananUnggulan: { layananUnggulan: {
content: '', content: ''
},
dokterdanTenagaMedis: {
name: '',
specialist: '',
jadwal: '',
}, },
dokterdanTenagaMedis: [] as string[],
fasilitasPendukung: { fasilitasPendukung: {
content: '', content: '',
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: '', content: '',
}, },
tarifDanLayanan: { tarifDanLayanan: [] as string[],
layanan: '',
tarif: '',
},
}; };
}; };
@@ -70,6 +64,11 @@ function CreateFasilitasKesehatan() {
} }
}; };
useShallowEffect(() => {
fasilitasKesehatanState.dokter.findMany.load();
fasilitasKesehatanState.tarif.findMany.load();
}, []);
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
{/* Header */} {/* Header */}
@@ -140,31 +139,25 @@ function CreateFasilitasKesehatan() {
/> />
</Box> </Box>
{/* Dokter dan Tenaga Medis */} {/* Dokter dan Tenaga Medis */}
<Box> <MultiSelect
<Text fz="md" fw="bold" mb={5}>Dokter dan Tenaga Medis</Text> label="Dokter & Tenaga Medis"
<TextInput placeholder="Pilih dokter / tenaga medis"
label="Nama Dokter" data={
placeholder="Masukkan nama dokter" fasilitasKesehatanState.dokter.findMany.data?.map((item) => ({
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name} label: item.name,
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)} value: item.id,
})) || []
}
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis}
onChange={(val: string[]) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis = val;
}}
searchable
clearable
required required
/> />
<TextInput
label="Spesialis"
placeholder="Masukkan spesialis"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
required
/>
<TextInput
label="Jadwal"
placeholder="Masukkan jadwal"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)}
required
/>
</Box>
{/* Fasilitas Pendukung */} {/* Fasilitas Pendukung */}
<Box> <Box>
@@ -175,6 +168,24 @@ function CreateFasilitasKesehatan() {
/> />
</Box> </Box>
<MultiSelect
label="Layanan"
placeholder="Pilih layanan"
data={
fasilitasKesehatanState.tarif.findMany.data?.map((item) => ({
label: item.layanan,
value: item.id,
})) || []
}
value={stateFasilitasKesehatan.create.form.tarifDanLayanan} // string[]
onChange={(val: string[]) => {
stateFasilitasKesehatan.create.form.tarifDanLayanan = val;
}}
searchable
clearable
required
/>
{/* Prosedur Pendaftaran */} {/* Prosedur Pendaftaran */}
<Box> <Box>
<Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text> <Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text>
@@ -184,24 +195,6 @@ function CreateFasilitasKesehatan() {
/> />
</Box> </Box>
{/* Tarif dan Layanan */}
<Box>
<Text fz="md" fw="bold" mb={5}>Tarif dan Layanan</Text>
<TextInput
label="Tarif"
placeholder="Masukkan tarif"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)}
required
/>
<TextInput
label="Layanan"
placeholder="Masukkan layanan"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)}
required
/>
</Box>
{/* Submit */} {/* Submit */}
<Group justify="right"> <Group justify="right">

View File

@@ -1,11 +1,241 @@
import React from 'react'; /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title
} from '@mantine/core';
import { TimeInput } from '@mantine/dates';
import { IconArrowBack, IconClock } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditDokterTenagaMedis() {
const state = useProxy(fasilitasKesehatanState.dokter);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
specialist: '',
jadwal: '',
jadwalLibur: '',
jamBukaOperasional: '',
jamTutupOperasional: '',
jamBukaLibur: '',
jamTutupLibur: '',
});
const [originalData, setOriginalData] = useState({
name: '',
specialist: '',
jadwal: '',
jadwalLibur: '',
jamBukaOperasional: '',
jamTutupOperasional: '',
jamBukaLibur: '',
jamTutupLibur: '',
});
// Load data
useEffect(() => {
const load = async () => {
const id = params?.id as string;
if (!id) return;
try {
await state.update.load(id);
const loadedData = state.update.form;
if (!loadedData) {
toast.error('Data tidak ditemukan');
return;
}
setFormData(loadedData);
setOriginalData(loadedData);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data fasilitas kesehatan');
}
};
load();
}, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
specialist: originalData.specialist,
jadwal: originalData.jadwal,
jadwalLibur: originalData.jadwalLibur,
jamBukaOperasional: originalData.jamBukaOperasional,
jamTutupOperasional: originalData.jamTutupOperasional,
jamBukaLibur: originalData.jamBukaLibur,
jamTutupLibur: originalData.jamTutupLibur,
});
toast.info("Form dikembalikan ke data awal");
};
const refBuka = useRef<HTMLInputElement>(null);
const refTutup = useRef<HTMLInputElement>(null);
const refBukaLibur = useRef<HTMLInputElement>(null);
const refTutupLibur = useRef<HTMLInputElement>(null);
const picker = (ref: any) => (
<ActionIcon variant="subtle" color="gray" onClick={() => ref.current?.showPicker()}>
<IconClock size={16} stroke={1.5} />
</ActionIcon>
);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Submit
const handleSubmit = async () => {
try {
setIsSubmitting(true);
state.update.form = { ...state.update.form, ...formData };
const success = await state.update.submit();
if (success) {
toast.success('Data berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis');
}
} catch (err) {
console.error(err);
toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsSubmitting(false);
}
};
function Page() {
return ( return (
<div> <Box px={{ base: 'sm', md: 'lg' }} py="md">
Page {/* Header */}
</div> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Fasilitas Kesehatan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Dokter"
placeholder="Masukkan nama dokter"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<TextInput
label="Jadwal"
placeholder="Masukkan jadwal"
value={formData.jadwal}
onChange={(e) => handleChange("jadwal", e.target.value)}
required
/>
<TextInput
label="Jadwal Libur"
placeholder="Masukkan jadwal libur"
value={formData.jadwalLibur}
onChange={(e) => handleChange("jadwalLibur", e.target.value)}
required
/>
<TimeInput
label="Jam Buka Operasional"
ref={refBuka}
rightSection={picker(refBuka)}
value={formData.jamBukaOperasional}
onChange={(e) => handleChange("jamBukaOperasional", e.target.value)}
required
/>
<TimeInput
label="Jam Tutup Operasional"
ref={refTutup}
rightSection={picker(refTutup)}
value={formData.jamTutupOperasional}
onChange={(e) => handleChange("jamTutupOperasional", e.target.value)}
required
/>
<TimeInput
label="Jam Buka Hari Libur"
ref={refBukaLibur}
rightSection={picker(refBukaLibur)}
value={formData.jamBukaLibur}
onChange={(e) => handleChange("jamBukaLibur", e.target.value)}
required
/>
<TimeInput
label="Jam Tutup Hari Libur"
ref={refTutupLibur}
rightSection={picker(refTutupLibur)}
value={formData.jamTutupLibur}
onChange={(e) => handleChange("jamTutupLibur", e.target.value)}
required
/>
{/* Tombol Simpan */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }
export default Page; export default EditDokterTenagaMedis;

View File

@@ -1,11 +1,165 @@
import React from 'react'; 'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function Page() { function DetailDokterTenagaMedis() {
const params = useParams();
const router = useRouter();
const state = useProxy(fasilitasKesehatanState.dokter);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
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/fasilitas_kesehatan/dokter-tenaga-medis'
);
}
};
if (!state.findUnique.data) {
return ( return (
<div> <Stack py={10}>
Page <Skeleton height={500} radius="md" />
</div> </Stack>
); );
} }
export default Page; const data = state.findUnique.data;
return (
<Box py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Dokter & Tenaga Medis
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Dokter</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="md" fw="bold">Specialist</Text>
<Text fz="md" c="dimmed">{data.specialist || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jadwal</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jadwal || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jadwal Libur</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jadwalLibur || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jam Buka Operasional</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamBukaOperasional || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jam Tutup Operasional</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamTutupOperasional || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jam Buka Libur</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamBukaLibur || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jam Tutup Libur</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamTutupLibur || '-' }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Button
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus dokter & tenaga medis ini?"
/>
</Box>
);
}
export default DetailDokterTenagaMedis;

View File

@@ -1,71 +1,184 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
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, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { ActionIcon, Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { TimeInput } from '@mantine/dates';
import { useParams, useRouter } from 'next/navigation'; import { IconArrowBack, IconClock } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateDokter() { function CreateDokter() {
const params = useParams()
const createState = useProxy(fasilitasKesehatanState.dokter) const createState = useProxy(fasilitasKesehatanState.dokter)
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
createState.create.create.form = { createState.create.create.form = {
name: "", name: "",
specialist: "", specialist: "",
jadwal: "", jadwal: "",
jadwalLibur: "",
jamBukaOperasional: "",
jamTutupOperasional: "",
jamBukaLibur: "",
jamTutupLibur: "",
}; };
}; };
const refBuka = useRef<HTMLInputElement>(null);
const refTutup = useRef<HTMLInputElement>(null);
const refBukaLibur = useRef<HTMLInputElement>(null);
const refTutupLibur = useRef<HTMLInputElement>(null);
const picker = (ref: any) => (
<ActionIcon variant="subtle" color="gray" onClick={() => ref.current?.showPicker()}>
<IconClock size={16} stroke={1.5} />
</ActionIcon>
);
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await createState.create.create.create(); await createState.create.create.create();
toast.success('Data berhasil disimpan');
resetForm(); resetForm();
router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`) router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`)
} catch (error) {
console.error(error);
toast.error('Gagal menyimpan data');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> <Title order={4} ml="sm" c="dark">
Tambah Data Dokter & Tenaga Medis
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Dokter</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>} label={"Nama Dokter"}
placeholder="masukkan nama dokter" placeholder="Masukkan nama dokter"
value={createState.create.create.form.name} value={createState.create.create.form.name}
onChange={(e) => { onChange={(e) => (createState.create.create.form.name = e.target.value)}
createState.create.create.form.name = e.target.value; required
}}
/> />
<Text fz="md" fw="bold">Specialist</Text>
{/* Informasi Umum */}
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>} label="Specialist"
placeholder="masukkan specialist" placeholder="Masukkan specialist"
value={createState.create.create.form.specialist} value={createState.create.create.form.specialist}
onChange={(e) => { onChange={(e) => (createState.create.create.form.specialist = e.target.value)}
createState.create.create.form.specialist = e.target.value; required
}}
/> />
<Box>
<Text fz="md" fw="bold">Jadwal</Text> <TextInput
<CreateEditor label="Jadwal"
placeholder="Masukkan jadwal"
value={createState.create.create.form.jadwal} value={createState.create.create.form.jadwal}
onChange={(htmlContent) => { onChange={(e) => (createState.create.create.form.jadwal = e.target.value)}
createState.create.create.form.jadwal = htmlContent; required
}}
/> />
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> <TextInput
Simpan label="Jadwal Libur"
placeholder="Masukkan jadwal libur"
value={createState.create.create.form.jadwalLibur}
onChange={(e) => (createState.create.create.form.jadwalLibur = e.target.value)}
required
/>
<TimeInput
label="Jam Buka Operasional"
ref={refBuka}
rightSection={picker(refBuka)}
value={createState.create.create.form.jamBukaOperasional}
onChange={(e) => (createState.create.create.form.jamBukaOperasional = e.target.value)}
required
/>
<TimeInput
label="Jam Tutup Operasional"
ref={refTutup}
rightSection={picker(refTutup)}
value={createState.create.create.form.jamTutupOperasional}
onChange={(e) => (createState.create.create.form.jamTutupOperasional = e.target.value)}
required
/>
<TimeInput
label="Jam Buka Hari Libur"
ref={refBukaLibur}
rightSection={picker(refBukaLibur)}
value={createState.create.create.form.jamBukaLibur}
onChange={(e) => (createState.create.create.form.jamBukaLibur = e.target.value)}
required
/>
<TimeInput
label="Jam Tutup Hari Libur"
ref={refTutupLibur}
rightSection={picker(refTutupLibur)}
value={createState.create.create.form.jamTutupLibur}
onChange={(e) => (createState.create.create.form.jamTutupLibur = e.target.value)}
required
/>
{/* Submit */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button> </Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, 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 HeaderSearch from '@/app/admin/(dashboard)/_com/header'; 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 fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import { useState } from 'react'; import { useState } from 'react';
@@ -18,7 +17,7 @@ function DokterTenagaMedis() {
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
</Box> </Box>
@@ -60,49 +59,101 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Fasilitas Kesehatan' <Title order={4}>Daftar Dokter dan Tenaga Medis</Title>
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`} <Button
/> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create'
)
}
>
Tambah Baru
</Button>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Fasilitas Kesehatan</TableTh> <TableTh>Nama Dokter</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Spesialis</TableTh>
<TableTh>Jam Operasional</TableTh> <TableTh>Jadwal</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.specialist}</TableTd>
<TableTd> <TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} /> <Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}> <Box w={150}>
<IconDeviceImacCog size={25} /> {item.specialist || '-'}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,9 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Center, Center,
Grid,
GridCol,
Group, Group,
Pagination, Pagination,
Paper, Paper,
@@ -16,30 +19,52 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title TextInput,
Title,
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
function FasilitasKesehatan() { function FasilitasKesehatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter()
return ( return (
<Box> <Box>
{/* Header Search */} <Grid mb={10}>
<HeaderSearch <GridCol span={{ base: 12, md: 8 }}>
title='Fasilitas Kesehatan' <Title order={3}>Fasilitas Kesehatan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<Group gap={"xs"}>
<Tooltip label="List Dokter" withArrow>
<ActionIcon onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')} size="lg" radius="xl" color="green.6">
<IconReportMedical size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label="List Tarif Layanan" withArrow>
<ActionIcon onClick={()=> router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')} size="lg" radius="xl" color="blue.6">
<IconCoin size={20} />
</ActionIcon>
</Tooltip>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder='Cari nama, alamat, atau jam operasional...' placeholder='Cari nama, alamat, atau jam operasional...'
searchIcon={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
w="133%"
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
</Paper>
</Group>
</GridCol>
</Grid>
<ListFasilitasKesehatan search={search} /> <ListFasilitasKesehatan search={search} />
</Box> </Box>
@@ -54,6 +79,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany; const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
@@ -93,8 +119,8 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Fasilitas Kesehatan</TableTh> <TableTh>Fasilitas Kesehatan</TableTh>
<TableTh>Dokter</TableTh> <TableTh>Jumlah Dokter</TableTh>
<TableTh>Layanan</TableTh> <TableTh>Jumlah Layanan</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -111,13 +137,17 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Box w={150}>
{item.dokterdantenagamedis?.name || '-'} {item.dokterdantenagamedis?.length
? `${item.dokterdantenagamedis.length} dokter`
: '-'}
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Box w={150}>
<Text truncate="end" lineClamp={1}> <Text truncate="end" lineClamp={1}>
{item.tarifdanlayanan?.layanan || '-'} {item.tarifdanlayanan?.length
? `${item.tarifdanlayanan.length} layanan`
: '-'}
</Text> </Text>
</Box> </Box>
</TableTd> </TableTd>
@@ -141,7 +171,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed">
Tidak ada fasilitas kesehatan yang cocok Tidak ada fasilitas kesehatan yang cocok
</Text> </Text>
</Center> </Center>

View File

@@ -0,0 +1,173 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
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 EditTarifLayanan() {
const editState = useProxy(fasilitasKesehatanState.tarif);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
tarif: '',
layanan: ''
});
const [formData, setFormData] = useState({
tarif: '',
layanan: ''
});
useEffect(() => {
const loadTarifLayanan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id);
if (data) {
setFormData({
tarif: data.tarif || '',
layanan: data.layanan || '',
});
setOriginalData({
tarif: data.tarif || '',
layanan: data.layanan || '',
});
}
} catch (error) {
console.error('Error loading tarif layanan:', error);
toast.error('Gagal memuat data tarif layanan');
}
};
loadTarifLayanan();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleResetForm = () => {
setFormData({
tarif: originalData.tarif,
layanan: originalData.layanan,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// update global state hanya saat submit
editState.update.form = {
...editState.update.form,
tarif: formData.tarif,
layanan: formData.layanan,
};
await editState.update.submit();
toast.success('Tarif Layanan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan');
} catch (error) {
console.error('Error updating tarif layanan:', error);
toast.error('Terjadi kesalahan saat memperbarui tarif layanan');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Tarif Layanan
</Title>
</Group>
{/* Form Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
name="layanan"
label="Layanan"
placeholder="Masukkan nama layanan"
value={formData.layanan}
onChange={(e) => handleChange('layanan', e.target.value)}
required
/>
<TextInput
name="tarif"
label="Tarif"
placeholder="Masukkan tarif layanan"
value={formData.tarif}
onChange={(e) => handleChange('tarif', e.target.value)}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditTarifLayanan;

View File

@@ -0,0 +1,119 @@
'use client';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateTarifLayanan() {
const createState = useProxy(fasilitasKesehatanState.tarif);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
tarif: '',
layanan: '',
};
};
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await createState.create.create();
resetForm();
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan');
} catch (error) {
console.error('Error creating tarif layanan:', error);
toast.error('Gagal menambahkan tarif layanan');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Tarif Layanan
</Title>
</Group>
{/* Form utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Layanan"
placeholder="Masukkan nama layanan"
value={createState.create.form.layanan || ''}
onChange={(e) => (createState.create.form.layanan = e.target.value)}
required
/>
<TextInput
label="Tarif"
placeholder="Masukkan tarif"
value={createState.create.form.tarif || ''}
onChange={(e) => (createState.create.form.tarif = e.target.value)}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateTarifLayanan;

View File

@@ -1,13 +1,13 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } 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 HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList'; 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 { useState } from 'react'; import { useState } from 'react';
@@ -18,12 +18,12 @@ function TarifLayanan() {
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
</Box> </Box>
<HeaderSearch <HeaderSearch
title='Dokter dan Tenaga Medis' title='Tarif dan Layanan'
placeholder='pencarian' placeholder='pencarian'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
@@ -35,8 +35,11 @@ function TarifLayanan() {
} }
function ListTarifLayanan({ search }: { search: string }) { function ListTarifLayanan({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.tarif);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const { const {
data, data,
loading, loading,
@@ -49,6 +52,15 @@ function ListTarifLayanan({ search }: { search: string }) {
load(page, 10, search) load(page, 10, search)
}, [page, search]) }, [page, search])
const handleDelete = () => {
if (selectedId) {
stateFasilitasKesehatan.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
};
const filteredData = data || [] const filteredData = data || []
if (loading || !data) { if (loading || !data) {
@@ -60,51 +72,116 @@ function ListTarifLayanan({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Fasilitas Kesehatan' <Title order={4}>Daftar Tarif dan Layanan</Title>
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`} <Button
/> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create'
)
}
>
Tambah Baru
</Button>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Fasilitas Kesehatan</TableTh> <TableTh>Layanan</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Tarif</TableTh>
<TableTh>Jam Operasional</TableTh> <TableTh>Edit</TableTh>
<TableTh>Detail</TableTh> <TableTh>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.specialist}</TableTd>
<TableTd> <TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} /> <Box w={150}>
{item.layanan || '-'}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}> <Box w={150}>
<IconDeviceImacCog size={25} /> <Text fw={500} truncate="end" lineClamp={1}>
{item.tarif}
</Text>
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateFasilitasKesehatan.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus tarif layanan ini?"
/>
</Box> </Box>
) )
} }

View File

@@ -1,19 +1,17 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
type FasilitasKesehatanInput = { const fasilitasKesehatanCreate = async (context: Context) => {
const body = (await context.body) as {
name: string; name: string;
informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string }; informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string };
layananUnggulan: { content: string }; layananUnggulan: { content: string };
dokterdanTenagaMedis: { name: string; specialist: string; jadwal: string }; dokterdanTenagaMedis: string[]; // ← ARRAY OF ID
fasilitasPendukung: { content: string }; fasilitasPendukung: { content: string };
prosedurPendaftaran: { content: string }; prosedurPendaftaran: { content: string };
tarifDanLayanan: { layanan: string; tarif: string }; tarifDanLayanan: string[]; // ← ARRAY OF ID
}; };
const fasilitasKesehatanCreate = async (context: Context) => {
const body = await context.body as FasilitasKesehatanInput;
const { const {
name, name,
informasiUmum, informasiUmum,
@@ -24,25 +22,30 @@ const fasilitasKesehatanCreate = async (context: Context) => {
tarifDanLayanan, tarifDanLayanan,
} = body; } = body;
// Buat masing-masing relasi terlebih dahulu // CREATE SINGLE DATA
const [createdInformasiUmum, createdLayananUnggulan, createdDokter, createdPendukung, createdProsedur, createdTarif] = await Promise.all([ const [createdInformasi, createdUnggulan, createdPendukung, createdProsedur] =
await Promise.all([
prisma.informasiUmum.create({ data: informasiUmum }), prisma.informasiUmum.create({ data: informasiUmum }),
prisma.layananUnggulan.create({ data: layananUnggulan }), prisma.layananUnggulan.create({ data: layananUnggulan }),
prisma.dokterdanTenagaMedis.create({ data: dokterdanTenagaMedis }),
prisma.fasilitasPendukung.create({ data: fasilitasPendukung }), prisma.fasilitasPendukung.create({ data: fasilitasPendukung }),
prisma.prosedurPendaftaran.create({ data: prosedurPendaftaran }), prisma.prosedurPendaftaran.create({ data: prosedurPendaftaran }),
prisma.tarifDanLayanan.create({ data: tarifDanLayanan }),
]); ]);
// ✅ CUKUP CONNECT KE ID YANG SUDAH ADA
const fasilitas = await prisma.fasilitasKesehatan.create({ const fasilitas = await prisma.fasilitasKesehatan.create({
data: { data: {
name, name,
informasiUmumId: createdInformasiUmum.id, informasiUmumId: createdInformasi.id,
layananUnggulanId: createdLayananUnggulan.id, layananUnggulanId: createdUnggulan.id,
dokterdanTenagaMedisId: createdDokter.id,
fasilitasPendukungId: createdPendukung.id, fasilitasPendukungId: createdPendukung.id,
prosedurPendaftaranId: createdProsedur.id, prosedurPendaftaranId: createdProsedur.id,
tarifDanLayananId: createdTarif.id,
dokterdantenagamedis: {
connect: dokterdanTenagaMedis.map(id => ({ id })), // ← langsung dari input
},
tarifdanlayanan: {
connect: tarifDanLayanan.map(id => ({ id })), // ← langsung dari input
},
}, },
include: { include: {
informasiumum: true, informasiumum: true,

View File

@@ -4,40 +4,12 @@ import { Context } from "elysia";
const fasilitasKesehatanDelete = async (context: Context) => { const fasilitasKesehatanDelete = async (context: Context) => {
const id = context.params?.id as string; const id = context.params?.id as string;
if (!id) { const data = await prisma.fasilitasKesehatan.findUnique({ where: { id } });
return { if (!data) return { status: 404, message: "Data tidak ditemukan" };
status: 400,
message: "ID tidak ditemukan",
}
}
const fasilitasKesehatan = await prisma.fasilitasKesehatan.findUnique({ await prisma.fasilitasKesehatan.delete({ where: { id } });
where: { id },
include: {
informasiumum: true,
layananunggulan: true,
dokterdantenagamedis: true,
fasilitaspendukung: true,
prosedurpendaftaran: true,
tarifdanlayanan: true,
}
})
if (!fasilitasKesehatan) { return { success: true, message: "Berhasil dihapus" };
return { };
status: 404,
message: "Fasilitas kesehatan tidak ditemukan",
}
}
await prisma.fasilitasKesehatan.delete({ export default fasilitasKesehatanDelete;
where: { id },
})
return {
status: 200,
success: true,
message: "Fasilitas kesehatan berhasil dihapus",
}
}
export default fasilitasKesehatanDelete

View File

@@ -5,6 +5,11 @@ type FormCreate = {
name: string; name: string;
specialist: string; specialist: string;
jadwal: string; jadwal: string;
jadwalLibur: string;
jamBukaOperasional: string;
jamTutupOperasional: string;
jamBukaLibur: string;
jamTutupLibur: string;
}; };
export default async function dokterTenagaMedisCreate(context: Context) { export default async function dokterTenagaMedisCreate(context: Context) {
@@ -15,11 +20,21 @@ export default async function dokterTenagaMedisCreate(context: Context) {
name: body.name, name: body.name,
specialist: body.specialist, specialist: body.specialist,
jadwal: body.jadwal, jadwal: body.jadwal,
jadwalLibur: body.jadwalLibur,
jamBukaOperasional: body.jamBukaOperasional,
jamTutupOperasional: body.jamTutupOperasional,
jamBukaLibur: body.jamBukaLibur,
jamTutupLibur: body.jamTutupLibur,
}, },
select: { select: {
name: true, name: true,
specialist: true, specialist: true,
jadwal: true, jadwal: true,
jadwalLibur: true,
jamBukaOperasional: true,
jamTutupOperasional: true,
jamBukaLibur: true,
jamTutupLibur: true,
} }
}); });

View File

@@ -19,6 +19,7 @@ async function dokterTenagaMedisFindMany(context: Context) {
{ name: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } },
{ specialist: { contains: search, mode: 'insensitive' } }, { specialist: { contains: search, mode: 'insensitive' } },
{ jadwal: { contains: search, mode: 'insensitive' } }, { jadwal: { contains: search, mode: 'insensitive' } },
{ jadwalLibur: { contains: search, mode: 'insensitive' } },
]; ];
} }

View File

@@ -19,6 +19,11 @@ const DokterTenagaMedis = new Elysia({
name: t.String(), name: t.String(),
specialist: t.String(), specialist: t.String(),
jadwal: t.String(), jadwal: t.String(),
jadwalLibur: t.String(),
jamBukaOperasional: t.String(),
jamTutupOperasional: t.String(),
jamBukaLibur: t.String(),
jamTutupLibur: t.String(),
}), }),
}) })
.put("/:id", dokterTenagaMedisUpdate, { .put("/:id", dokterTenagaMedisUpdate, {
@@ -26,6 +31,11 @@ const DokterTenagaMedis = new Elysia({
name: t.String(), name: t.String(),
specialist: t.String(), specialist: t.String(),
jadwal: t.String(), jadwal: t.String(),
jadwalLibur: t.String(),
jamBukaOperasional: t.String(),
jamTutupOperasional: t.String(),
jamBukaLibur: t.String(),
jamTutupLibur: t.String(),
}), }),
}) })
.delete("/del/:id", dokterTenagaMedisDelete) .delete("/del/:id", dokterTenagaMedisDelete)

View File

@@ -5,6 +5,11 @@ type FormUpdate = {
name: string; name: string;
specialist: string; specialist: string;
jadwal: string; jadwal: string;
jadwalLibur: string;
jamBukaOperasional: string;
jamTutupOperasional: string;
jamBukaLibur: string;
jamTutupLibur: string;
} }
export default async function dokterTenagaMedisUpdate(context: Context) { export default async function dokterTenagaMedisUpdate(context: Context) {
@@ -18,6 +23,12 @@ export default async function dokterTenagaMedisUpdate(context: Context) {
name: body.name, name: body.name,
specialist: body.specialist, specialist: body.specialist,
jadwal: body.jadwal, jadwal: body.jadwal,
jadwalLibur: body.jadwalLibur,
jamBukaOperasional: body.jamBukaOperasional,
jamTutupOperasional: body.jamTutupOperasional,
jamBukaLibur: body.jamBukaLibur,
jamTutupLibur: body.jamTutupLibur,
}, },
}); });
return { return {

View File

@@ -1,6 +1,6 @@
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import fasilitasKesehatanCreate from "./create"; import fasilitasKesehatanCreate from "./create";
import findManyFasilitasKesehatan from "./findMany"; import fasilitasKesehatanFindMany from "./findMany";
import findUniqueFasilitasKesehatan from "./findUnique"; import findUniqueFasilitasKesehatan from "./findUnique";
import fasilitasKesehatanUpdate from "./updt"; import fasilitasKesehatanUpdate from "./updt";
import fasilitasKesehatanDelete from "./del"; import fasilitasKesehatanDelete from "./del";
@@ -9,42 +9,61 @@ const FasilitasKesehatan = new Elysia({
prefix: "fasilitas-kesehatan", prefix: "fasilitas-kesehatan",
tags: ["Kesehatan/Fasilitas Kesehatan"], tags: ["Kesehatan/Fasilitas Kesehatan"],
}) })
// ==========================
// CREATE
// ==========================
.post("/create", fasilitasKesehatanCreate, { .post("/create", fasilitasKesehatanCreate, {
body: t.Object({ body: t.Object({
name: t.String(), name: t.String(),
informasiUmum: t.Object({ informasiUmum: t.Object({
fasilitas: t.String(), fasilitas: t.String(),
alamat: t.String(), alamat: t.String(),
jamOperasional: t.String(), jamOperasional: t.String(),
}), }),
layananUnggulan: t.Object({ layananUnggulan: t.Object({
content: t.String(), content: t.String(),
}), }),
dokterdanTenagaMedis: t.Object({
name: t.String(), dokterdanTenagaMedis: t.Array(t.String()), // FIX karena create pakai array of string
specialist: t.String(),
jadwal: t.String(),
}),
fasilitasPendukung: t.Object({ fasilitasPendukung: t.Object({
content: t.String(), content: t.String(),
}), }),
prosedurPendaftaran: t.Object({ prosedurPendaftaran: t.Object({
content: t.String(), content: t.String(),
}), }),
tarifDanLayanan: t.Object({
layanan: t.String(), tarifDanLayanan: t.Array(t.String()), // FIX karena create pakai array of string
tarif: t.String(),
}),
}), }),
}) })
.get("/find-many", findManyFasilitasKesehatan)
// ==========================
// FIND MANY
// ==========================
.get("/find-many", fasilitasKesehatanFindMany)
// ==========================
// DELETE
// ==========================
.delete("/del/:id", fasilitasKesehatanDelete) .delete("/del/:id", fasilitasKesehatanDelete)
// ==========================
// FIND UNIQUE
// ==========================
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await findUniqueFasilitasKesehatan( const response = await findUniqueFasilitasKesehatan(
new Request(context.request) new Request(context.request)
); );
return response; return response;
}) })
// ==========================
// UPDATE
// ==========================
.put( .put(
"/:id", "/:id",
async (context) => { async (context) => {
@@ -54,29 +73,30 @@ const FasilitasKesehatan = new Elysia({
{ {
body: t.Object({ body: t.Object({
name: t.String(), name: t.String(),
informasiUmum: t.Object({ informasiUmum: t.Object({
fasilitas: t.String(), fasilitas: t.String(),
alamat: t.String(), alamat: t.String(),
jamOperasional: t.String(), jamOperasional: t.String(),
}), }),
layananUnggulan: t.Object({ layananUnggulan: t.Object({
content: t.String(), content: t.String(),
}), }),
dokterdanTenagaMedis: t.Object({
name: t.String(), // FIX → harus array of string (ID dokter)
specialist: t.String(), dokterdanTenagaMedis: t.Array(t.String()),
jadwal: t.String(),
}),
fasilitasPendukung: t.Object({ fasilitasPendukung: t.Object({
content: t.String(), content: t.String(),
}), }),
prosedurPendaftaran: t.Object({ prosedurPendaftaran: t.Object({
content: t.String(), content: t.String(),
}), }),
tarifDanLayanan: t.Object({
layanan: t.String(), // FIX → harus array of string (ID tarif)
tarif: t.String(), tarifDanLayanan: t.Array(t.String()),
}),
}), }),
} }
); );

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
layanan: string;
tarif: string;
};
export default async function tarifLayananCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.tarifDanLayanan.create({
data: {
layanan: body.layanan,
tarif: body.tarif,
},
select: {
layanan: true,
tarif: true,
}
});
return {
success: true,
message: "Sukses menambahkan dokter tenaga medis",
data: created,
};
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function tarifLayananDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
};
}
const existing = await prisma.tarifDanLayanan.findUnique({
where: {
id: id,
},
});
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
};
}
const deleted = await prisma.tarifDanLayanan.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 tarifLayananFindMany(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 = [
{ layanan: { contains: search, mode: 'insensitive' } },
{ tarif: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.tarifDanLayanan.findMany({
where,
skip,
take: limit,
orderBy: { layanan: 'asc' },
}),
prisma.tarifDanLayanan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data tarif layanan 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 tarif layanan",
};
}
}
export default tarifLayananFindMany;

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
export default async function tarifLayananFindUnique(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.tarifDanLayanan.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,30 @@
import Elysia, { t } from "elysia";
import tarifLayananCreate from "./create";
import tarifLayananFindMany from "./findMany";
import tarifLayananFindUnique from "./findUnique";
import tarifLayananDelete from "./del";
import tarifLayananUpdate from "./updt";
const TarifLayanan = new Elysia({
prefix: "/tarifdanlayanan",
tags: ["Data Kesehatan/Fasilitas Kesehatan/Tarif Layanan"]
})
.get("/:id", async (context) => {
const response = await tarifLayananFindUnique(new Request(context.request));
return response;
})
.get("/findMany", tarifLayananFindMany)
.post("/create", tarifLayananCreate, {
body: t.Object({
tarif: t.String(),
layanan: t.String()
}),
})
.put("/:id", tarifLayananUpdate, {
body: t.Object({
tarif: t.String(),
layanan: t.String(),
}),
})
.delete("/del/:id", tarifLayananDelete)
export default TarifLayanan

View File

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

View File

@@ -5,32 +5,26 @@ type FasilitasKesehatanInput = {
name: string; name: string;
informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string }; informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string };
layananUnggulan: { content: string }; layananUnggulan: { content: string };
dokterdanTenagaMedis: { name: string; specialist: string; jadwal: string }; dokterdanTenagaMedis: string[]; // ← ID saja
fasilitasPendukung: { content: string }; fasilitasPendukung: { content: string };
prosedurPendaftaran: { content: string }; prosedurPendaftaran: { content: string };
tarifDanLayanan: { layanan: string; tarif: string }; tarifDanLayanan: string[]; // ← ID saja
}; };
const fasilitasKesehatanUpdate = async (context: Context) => { const fasilitasKesehatanUpdate = async (context: Context) => {
const id = context.params?.id as string; const id = context.params?.id as string;
const body = await context.body as FasilitasKesehatanInput; const body = (await context.body) as FasilitasKesehatanInput;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const existing = await prisma.fasilitasKesehatan.findUnique({ const existing = await prisma.fasilitasKesehatan.findUnique({
where: { id }, where: { id },
include: {
dokterdantenagamedis: true,
tarifdanlayanan: true,
},
}); });
if (!existing) { if (!existing) {
return new Response( return { success: false, message: "Data tidak ditemukan" };
JSON.stringify({ success: false, message: "Data not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
} }
const { const {
@@ -43,38 +37,46 @@ const fasilitasKesehatanUpdate = async (context: Context) => {
tarifDanLayanan, tarifDanLayanan,
} = body; } = body;
// Update data masing-masing relasi // update relasi 1-1
await Promise.all([ await Promise.all([
prisma.informasiUmum.update({ prisma.informasiUmum.update({
where: { id: existing.informasiUmumId }, where: { id: existing.informasiUmumId },
data: informasiUmum, data: informasiUmum,
}), }),
prisma.layananUnggulan.update({ prisma.layananUnggulan.update({
where: { id: existing.layananUnggulanId }, where: { id: existing.layananUnggulanId },
data: layananUnggulan, data: layananUnggulan,
}), }),
prisma.dokterdanTenagaMedis.update({
where: { id: existing.dokterdanTenagaMedisId },
data: dokterdanTenagaMedis,
}),
prisma.fasilitasPendukung.update({ prisma.fasilitasPendukung.update({
where: { id: existing.fasilitasPendukungId }, where: { id: existing.fasilitasPendukungId },
data: fasilitasPendukung, data: fasilitasPendukung,
}), }),
prisma.prosedurPendaftaran.update({ prisma.prosedurPendaftaran.update({
where: { id: existing.prosedurPendaftaranId }, where: { id: existing.prosedurPendaftaranId },
data: prosedurPendaftaran, data: prosedurPendaftaran,
}), }),
prisma.tarifDanLayanan.update({
where: { id: existing.tarifDanLayananId },
data: tarifDanLayanan,
}),
]); ]);
// Update main record // update m2m
const updated = await prisma.fasilitasKesehatan.update({ const updated = await prisma.fasilitasKesehatan.update({
where: { id }, where: { id },
data: { name },
data: {
name,
// reset dokter lama → ganti baru
dokterdantenagamedis: {
set: dokterdanTenagaMedis.map((id) => ({ id })),
},
tarifdanlayanan: {
set: tarifDanLayanan.map((id) => ({ id })),
},
},
include: { include: {
informasiumum: true, informasiumum: true,
layananunggulan: true, layananunggulan: true,
@@ -87,7 +89,7 @@ const fasilitasKesehatanUpdate = async (context: Context) => {
return { return {
success: true, success: true,
message: "Fasilitas berhasil diupdate", message: "Fasilitas diupdate",
data: updated, data: updated,
}; };
}; };

View File

@@ -20,6 +20,7 @@ import Kelahiran from "./data_kesehatan_warga/persentase_kelahiran_kematian/kela
import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian"; import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian";
import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis"; import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis";
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran"; import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layanan";
const Kesehatan = new Elysia({ const Kesehatan = new Elysia({
@@ -46,5 +47,6 @@ const Kesehatan = new Elysia({
.use(Kelahiran) .use(Kelahiran)
.use(Kematian) .use(Kematian)
.use(DokterTenagaMedis) .use(DokterTenagaMedis)
.use(TarifLayanan)
.use(PendaftaranJadwalKegiatan) .use(PendaftaranJadwalKegiatan)
export default Kesehatan; export default Kesehatan;

View File

@@ -557,25 +557,37 @@ export default async function searchFindMany(context: Context) {
], ],
}, },
layananunggulan: { content: { contains: query, mode: "insensitive" } }, layananunggulan: { content: { contains: query, mode: "insensitive" } },
dokterdantenagamedis: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ specialist: { contains: query, mode: "insensitive" } },
{ jadwal: { contains: query, mode: "insensitive" } },
],
},
fasilitaspendukung: { fasilitaspendukung: {
content: { contains: query, mode: "insensitive" }, content: { contains: query, mode: "insensitive" },
}, },
prosedurpendaftaran: { prosedurpendaftaran: {
content: { contains: query, mode: "insensitive" }, content: { contains: query, mode: "insensitive" },
}, },
tarifdanlayanan: {
OR: [
{ layanan: { contains: query, mode: "insensitive" } },
{ tarif: { contains: query, mode: "insensitive" } },
],
}, },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "doktertenagamedis") {
const data = await prisma.dokterdanTenagaMedis.findMany({
where: {
name: { contains: query, mode: "insensitive" },
specialist: { contains: query, mode: "insensitive" },
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "tarifdanlayanan") {
const data = await prisma.tarifDanLayanan.findMany({
where: {
layanan: { contains: query, mode: "insensitive" },
tarif: { contains: query, mode: "insensitive" },
}, },
skip, skip,
take: limitNum, take: limitNum,
@@ -1567,6 +1579,8 @@ export default async function searchFindMany(context: Context) {
jenisProgramYangDiselenggarakan, jenisProgramYangDiselenggarakan,
dataPerpustakaan, dataPerpustakaan,
dataPendidikan, dataPendidikan,
dokterDanTenagaMedis,
tarifDanLayanan
] = await Promise.all([ ] = await Promise.all([
prisma.pejabatDesa.findMany({ prisma.pejabatDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } }, where: { name: { contains: query, mode: "insensitive" } },
@@ -1894,25 +1908,27 @@ export default async function searchFindMany(context: Context) {
], ],
}, },
layananunggulan: { content: { contains: query, mode: "insensitive" } }, layananunggulan: { content: { contains: query, mode: "insensitive" } },
dokterdantenagamedis: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ specialist: { contains: query, mode: "insensitive" } },
{ jadwal: { contains: query, mode: "insensitive" } },
],
},
fasilitaspendukung: { fasilitaspendukung: {
content: { contains: query, mode: "insensitive" }, content: { contains: query, mode: "insensitive" },
}, },
prosedurpendaftaran: { prosedurpendaftaran: {
content: { contains: query, mode: "insensitive" }, content: { contains: query, mode: "insensitive" },
}, },
tarifdanlayanan: {
OR: [
{ layanan: { contains: query, mode: "insensitive" } },
{ tarif: { contains: query, mode: "insensitive" } },
],
}, },
take: limitNum,
}),
prisma.dokterdanTenagaMedis.findMany({
where: {
name: { contains: query, mode: "insensitive" },
specialist: { contains: query, mode: "insensitive" },
},
take: limitNum,
}),
prisma.tarifDanLayanan.findMany({
where: {
tarif: { contains: query, mode: "insensitive" },
layanan: { contains: query, mode: "insensitive" },
}, },
take: limitNum, take: limitNum,
}), }),
@@ -2559,6 +2575,8 @@ export default async function searchFindMany(context: Context) {
...penghargaan.map((b) => ({ type: "penghargaan", ...b })), ...penghargaan.map((b) => ({ type: "penghargaan", ...b })),
...posyandu.map((b) => ({ type: "posyandu", ...b })), ...posyandu.map((b) => ({ type: "posyandu", ...b })),
...fasilitasKesehatan.map((b) => ({ type: "fasilitasKesehatan", ...b })), ...fasilitasKesehatan.map((b) => ({ type: "fasilitasKesehatan", ...b })),
...dokterDanTenagaMedis.map((b) => ({ type: "dokterdanTenagaMedis", ...b })),
...tarifDanLayanan.map((b) => ({ type: "tarifDanLayanan", ...b })),
...jadwalKegiatan.map((b) => ({ type: "jadwalKegiatan", ...b })), ...jadwalKegiatan.map((b) => ({ type: "jadwalKegiatan", ...b })),
...artikelKesehatan.map((b) => ({ type: "artikelKesehatan", ...b })), ...artikelKesehatan.map((b) => ({ type: "artikelKesehatan", ...b })),
...puskesmas.map((b) => ({ type: "puskesmas", ...b })), ...puskesmas.map((b) => ({ type: "puskesmas", ...b })),

View File

@@ -42,9 +42,7 @@ function Page() {
const alamat = data?.informasiumum?.alamat || '-'; const alamat = data?.informasiumum?.alamat || '-';
const jam = data?.informasiumum?.jamOperasional || '-'; const jam = data?.informasiumum?.jamOperasional || '-';
const layananUnggulan = data?.layananunggulan?.content || ''; const layananUnggulan = data?.layananunggulan?.content || '';
const tenaga = data?.dokterdantenagamedis || null;
const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || ''; const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || '';
const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null;
const kontak = (data?.kontak as Kontak) || { const kontak = (data?.kontak as Kontak) || {
telepon: '(0361) 123456', telepon: '(0361) 123456',
whatsapp: '6289647037426', whatsapp: '6289647037426',
@@ -211,7 +209,7 @@ function Page() {
<Card radius="xl" p="lg" withBorder> <Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={4}>Dokter & Tenaga Medis</Title> <Title order={4}>Dokter & Tenaga Medis</Title>
<Table highlightOnHover withTableBorder withColumnBorders stickyHeader stickyHeaderOffset={0} aria-label="Tabel Dokter"> <Table highlightOnHover withTableBorder withColumnBorders aria-label="Tabel Dokter">
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh>Nama</TableTh>
@@ -220,12 +218,19 @@ function Page() {
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{tenaga ? ( {Array.isArray(data?.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? (
<TableTr> data.dokterdantenagamedis.map((dokter: any) => (
<TableTd><Group gap="xs"><IconUser size={16} /><Text>{tenaga?.name || '-'}</Text></Group></TableTd> <TableTr key={dokter.id}>
<TableTd>{tenaga?.specialist || '-'}</TableTd> <TableTd>
<TableTd>{tenaga?.jadwal || '-'}</TableTd> <Group gap="xs">
<IconUser size={16} />
<Text>{dokter.name || '-'}</Text>
</Group>
</TableTd>
<TableTd>{dokter.specialist || '-'}</TableTd>
<TableTd>{dokter.jadwal || '-'}</TableTd>
</TableTr> </TableTr>
))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
@@ -270,11 +275,13 @@ function Page() {
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{tarif ? ( {Array.isArray(data?.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
<TableTr> data.tarifdanlayanan.map((item: any) => (
<TableTd>{tarif?.layanan || '-'}</TableTd> <TableTr key={item.id}>
<TableTd>{formatRupiah(tarif?.tarif)}</TableTd> <TableTd>{item.layanan || '-'}</TableTd>
<TableTd>{formatRupiah(item.tarif)}</TableTd>
</TableTr> </TableTr>
))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={2}> <TableTd colSpan={2}>

View File

@@ -50,7 +50,9 @@ function SosmedView({
loading="lazy" loading="lazy"
src={src} src={src}
alt={item.name} alt={item.name}
fit={item.image?.link ? "cover" : "contain"} w={24}
h={24}
fit="contain"
/> />
); );
} }

View File

@@ -30,6 +30,8 @@ const getDetailUrl = (item: { type?: string; id: string | number; [key: string]:
penghargaan: () => '/darmasaba/desa/penghargaan', penghargaan: () => '/darmasaba/desa/penghargaan',
posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`, posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`,
fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
dokterDanTenagaMedis: () => '/darmasaba/kesehatan/data-kesehatan-warga',
tarifDanLayanan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
puskesmas: () => '/darmasaba/kesehatan/puskesmas', puskesmas: () => '/darmasaba/kesehatan/puskesmas',