Compare commits
1 Commits
nico/3-des
...
nico/test-
| Author | SHA1 | Date | |
|---|---|---|---|
| 091c33a73c |
@@ -136,7 +136,6 @@ model MediaSosial {
|
|||||||
name String
|
name String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
icon String?
|
|
||||||
iconUrl String? @db.VarChar(255)
|
iconUrl String? @db.VarChar(255)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -783,22 +782,24 @@ model Penghargaan {
|
|||||||
|
|
||||||
// ========================================= FASILITAS KESEHATAN ========================================= //
|
// ========================================= FASILITAS KESEHATAN ========================================= //
|
||||||
model FasilitasKesehatan {
|
model FasilitasKesehatan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
|
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
|
||||||
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("Dokter")
|
dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id])
|
||||||
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
|
dokterdanTenagaMedisId String
|
||||||
fasilitasPendukungId String
|
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
|
||||||
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
|
fasilitasPendukungId String
|
||||||
prosedurPendaftaranId String
|
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
|
||||||
tarifdanlayanan TarifDanLayanan[] @relation("Tarif")
|
prosedurPendaftaranId String
|
||||||
|
tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id])
|
||||||
|
tarifDanLayananId String
|
||||||
}
|
}
|
||||||
|
|
||||||
model InformasiUmum {
|
model InformasiUmum {
|
||||||
@@ -824,20 +825,15 @@ model LayananUnggulan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model DokterdanTenagaMedis {
|
model DokterdanTenagaMedis {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
specialist String
|
specialist String
|
||||||
jadwal String
|
jadwal String
|
||||||
jadwalLibur String
|
createdAt DateTime @default(now())
|
||||||
jamBukaOperasional String
|
updatedAt DateTime @updatedAt
|
||||||
jamTutupOperasional String
|
deletedAt DateTime @default(now())
|
||||||
jamBukaLibur String
|
isActive Boolean @default(true)
|
||||||
jamTutupLibur String
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
FasilitasKesehatan FasilitasKesehatan[] @relation("Dokter")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model FasilitasPendukung {
|
model FasilitasPendukung {
|
||||||
@@ -868,7 +864,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[] @relation("Tarif")
|
FasilitasKesehatan FasilitasKesehatan[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= JADWAL KEGIATAN ========================================= //
|
// ========================================= JADWAL KEGIATAN ========================================= //
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Box, Image, Select, rem } from '@mantine/core';
|
|
||||||
|
|
||||||
const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
type SosmedKey = keyof typeof sosmedMap;
|
|
||||||
|
|
||||||
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
|
||||||
value,
|
|
||||||
label: item.label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SelectSosialMedia({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: SosmedKey;
|
|
||||||
onChange: (value: SosmedKey) => void;
|
|
||||||
}) {
|
|
||||||
const selected = value;
|
|
||||||
const selectedImage = sosmedMap[selected]?.src;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box maw={300}>
|
|
||||||
<Select
|
|
||||||
placeholder="Pilih sosial media"
|
|
||||||
value={selected}
|
|
||||||
data={sosmedList}
|
|
||||||
searchable={false}
|
|
||||||
withCheckIcon={false}
|
|
||||||
onChange={(val) => val && onChange(val as SosmedKey)}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: rem(16),
|
|
||||||
paddingLeft: 36,
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
left: 10,
|
|
||||||
right: 'auto',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 🔥 PREVIEW DIPISAH DI LUAR SELECT */}
|
|
||||||
{selectedImage && (
|
|
||||||
<Box mt="md">
|
|
||||||
<Image
|
|
||||||
alt=""
|
|
||||||
src={selectedImage}
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #eee',
|
|
||||||
padding: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Box, Select } from '@mantine/core';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
type SosmedKey = keyof typeof sosmedMap;
|
|
||||||
|
|
||||||
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
|
||||||
value,
|
|
||||||
label: item.label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SelectSocialMediaEdit({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (val: SosmedKey) => void;
|
|
||||||
}) {
|
|
||||||
const [selected, setSelected] = useState<SosmedKey>('facebook');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value && sosmedMap[value as SosmedKey]) {
|
|
||||||
setSelected(value as SosmedKey);
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Select
|
|
||||||
label="Jenis Media Sosial"
|
|
||||||
value={selected}
|
|
||||||
data={sosmedList}
|
|
||||||
searchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (!val) return;
|
|
||||||
setSelected(val as SosmedKey);
|
|
||||||
onChange(val as SosmedKey);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,30 +9,29 @@ 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: z.string().min(1, "Fasilitas harus diisi"),
|
||||||
alamat: z.string().min(1),
|
alamat: z.string().min(1, "Alamat harus diisi"),
|
||||||
jamOperasional: z.string().min(1),
|
jamOperasional: z.string().min(1, "Jam operasional harus diisi"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
layananUnggulan: z.object({
|
layananUnggulan: z.object({
|
||||||
content: z.string().min(1),
|
content: z.string().min(1, "Layanan unggulan harus diisi"),
|
||||||
|
}),
|
||||||
|
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),
|
content: z.string().min(1, "Fasilitas pendukung harus diisi"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
prosedurPendaftaran: z.object({
|
prosedurPendaftaran: z.object({
|
||||||
content: z.string().min(1),
|
content: z.string().min(1, "Prosedur pendaftaran harus diisi"),
|
||||||
|
}),
|
||||||
|
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
|
||||||
@@ -46,34 +45,21 @@ const defaultForm = {
|
|||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
|
dokterdanTenagaMedis: {
|
||||||
dokterdanTenagaMedis: [] as string[], // ← array kosong
|
name: "",
|
||||||
tarifDanLayanan: [] as string[], // ← array kosong
|
specialist: "",
|
||||||
|
jadwal: "",
|
||||||
|
},
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
};
|
tarifDanLayanan: {
|
||||||
|
layanan: "",
|
||||||
type DokterItem = {
|
tarif: "",
|
||||||
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({
|
||||||
@@ -200,26 +186,33 @@ 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;
|
|
||||||
this.form = {
|
fasilitasKesehatan.edit.id = data.id;
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
// map relasi -> array of IDs
|
tarifDanLayanan: {
|
||||||
layananUnggulan: {
|
layanan: data.tarifdanlayanan.layanan,
|
||||||
content: data.layananunggulan.content,
|
tarif: data.tarifdanlayanan.tarif,
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis: data.dokterdantenagamedis?.map((v: DokterItem) => v.id) ?? [],
|
|
||||||
tarifDanLayanan: data.tarifdanlayanan?.map((v: TarifItem) => v.id) ?? [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
@@ -245,15 +238,22 @@ const fasilitasKesehatan = proxy({
|
|||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
|
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis:
|
dokterdanTenagaMedis: {
|
||||||
fasilitasKesehatan.edit.form.dokterdanTenagaMedis,
|
name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
|
||||||
|
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: fasilitasKesehatan.edit.form.tarifDanLayanan,
|
tarifDanLayanan: {
|
||||||
|
layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
|
||||||
|
tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -320,26 +320,12 @@ 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({
|
||||||
@@ -477,11 +463,6 @@ 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 {
|
||||||
@@ -506,11 +487,6 @@ 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);
|
||||||
@@ -591,255 +567,9 @@ 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;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const programInovasi = proxy({
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
link: "",
|
link: ""
|
||||||
} as ProgramInovasiForm,
|
} as ProgramInovasiForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
@@ -71,21 +71,20 @@ const programInovasi = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => {
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
// Change to arrow function
|
programInovasi.findMany.loading = true; // Use the full path to access the property
|
||||||
programInovasi.findMany.loading = true; // Use the full path to access the property
|
|
||||||
programInovasi.findMany.page = page;
|
programInovasi.findMany.page = page;
|
||||||
programInovasi.findMany.search = search;
|
programInovasi.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.programinovasi[
|
const res = await ApiFetch.api.landingpage.programinovasi[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query,
|
query
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
programInovasi.findMany.data = res.data.data || [];
|
programInovasi.findMany.data = res.data.data || [];
|
||||||
programInovasi.findMany.total = res.data.total || 0;
|
programInovasi.findMany.total = res.data.total || 0;
|
||||||
@@ -390,10 +389,7 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure ID is properly encoded in the URL
|
// Ensure ID is properly encoded in the URL
|
||||||
const url = new URL(
|
const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
|
||||||
`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`,
|
|
||||||
window.location.origin
|
|
||||||
);
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -442,19 +438,16 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
const templateMediaSosial = z.object({
|
const templateMediaSosial = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().nullable().optional(),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
||||||
icon: z.string().nullable().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type MediaSosialForm = {
|
type MediaSosialForm = {
|
||||||
name: string;
|
name: string;
|
||||||
imageId: string | null; // boleh null
|
imageId: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
icon: string | null; // boleh null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mediaSosial = proxy({
|
const mediaSosial = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as MediaSosialForm,
|
form: {} as MediaSosialForm,
|
||||||
@@ -462,10 +455,9 @@ const mediaSosial = proxy({
|
|||||||
async create() {
|
async create() {
|
||||||
// Ensure all required fields are non-null
|
// Ensure all required fields are non-null
|
||||||
const formData = {
|
const formData = {
|
||||||
name: mediaSosial.create.form.name ?? "",
|
name: mediaSosial.create.form.name || "",
|
||||||
imageId: mediaSosial.create.form.imageId ?? null, // FIXED
|
imageId: mediaSosial.create.form.imageId || "",
|
||||||
iconUrl: mediaSosial.create.form.iconUrl ?? "",
|
iconUrl: mediaSosial.create.form.iconUrl || "",
|
||||||
icon: mediaSosial.create.form.icon ?? null, // FIXED
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cek = templateMediaSosial.safeParse(formData);
|
const cek = templateMediaSosial.safeParse(formData);
|
||||||
@@ -500,19 +492,20 @@ const mediaSosial = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => {
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
// Change to arrow function
|
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
||||||
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
|
||||||
mediaSosial.findMany.page = page;
|
mediaSosial.findMany.page = page;
|
||||||
mediaSosial.findMany.search = search;
|
mediaSosial.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.mediasosial["findMany"].get({
|
const res = await ApiFetch.api.landingpage.mediasosial[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
mediaSosial.findMany.data = res.data.data || [];
|
mediaSosial.findMany.data = res.data.data || [];
|
||||||
mediaSosial.findMany.total = res.data.total || 0;
|
mediaSosial.findMany.total = res.data.total || 0;
|
||||||
@@ -544,7 +537,7 @@ const mediaSosial = proxy({
|
|||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.update.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
||||||
@@ -593,72 +586,66 @@ const mediaSosial = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: "",
|
||||||
form: {} as MediaSosialForm,
|
form: {} as MediaSosialForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
||||||
|
|
||||||
if (result?.success) {
|
try {
|
||||||
const data = result.data;
|
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
||||||
this.id = data.id;
|
method: "GET",
|
||||||
this.form = {
|
headers: {
|
||||||
name: data.name || "",
|
"Content-Type": "application/json",
|
||||||
imageId: data.imageId || null,
|
},
|
||||||
iconUrl: data.iconUrl || "",
|
});
|
||||||
icon: data.icon || null,
|
|
||||||
|
if (!response.ok) {
|
||||||
};
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
return data;
|
}
|
||||||
} else {
|
|
||||||
throw new Error(
|
const result = await response.json();
|
||||||
result?.message || "Gagal mengambil data media sosial"
|
|
||||||
);
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
name: data.name || "",
|
||||||
|
imageId: data.imageId || "",
|
||||||
|
iconUrl: data.iconUrl || "",
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal mengambil data media sosial");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error((error as Error).message);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
||||||
|
} finally {
|
||||||
|
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error((error as Error).message);
|
|
||||||
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
async update() {
|
||||||
} finally {
|
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
|
||||||
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
if (!cek.success) {
|
||||||
}
|
const err = `[${cek.error.issues
|
||||||
},
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
async update() {
|
toast.error(err);
|
||||||
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
|
return false;
|
||||||
if (!cek.success) {
|
}
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
try {
|
||||||
.join("\n")}] required`;
|
mediaSosial.update.loading = true;
|
||||||
toast.error(err);
|
|
||||||
return false;
|
const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, {
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
mediaSosial.update.loading = true;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/landingpage/mediasosial/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -667,40 +654,38 @@ const mediaSosial = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
iconUrl: this.form.iconUrl,
|
iconUrl: this.form.iconUrl,
|
||||||
icon: this.form.icon,
|
|
||||||
}),
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const result = await response.json();
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
if (result.success) {
|
||||||
throw new Error(
|
toast.success("Berhasil update media sosial");
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
await mediaSosial.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update media sosial");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating media sosial:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update media sosial"
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
mediaSosial.update.loading = false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update media sosial");
|
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || "Gagal update media sosial");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating media sosial:", error);
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Terjadi kesalahan saat update media sosial"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
mediaSosial.update.loading = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const profileLandingPageState = proxy({
|
const profileLandingPageState = proxy({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
|
|
||||||
|
import { apiFetchLogin } from '@/app/api/[auth]/_lib/api_fetch_auth';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// app/registrasi/page.tsx
|
// app/registrasi/page.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
|
import { apiFetchRegister } from '@/app/api/[auth]/_lib/api_fetch_auth';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +18,6 @@ export default function Registrasi() {
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
const [agree, setAgree] = useState(false)
|
|
||||||
|
|
||||||
// Ambil data dari localStorage (dari login)
|
// Ambil data dari localStorage (dari login)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,11 +46,6 @@ export default function Registrasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agree) {
|
|
||||||
toast.error("Anda harus menyetujui syarat dan ketentuan!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// ✅ Hanya kirim username & nomor → dapat kodeId
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
@@ -98,8 +92,8 @@ export default function Registrasi() {
|
|||||||
username.length > 0 && username.length < 5
|
username.length > 0 && username.length < 5
|
||||||
? 'Minimal 5 karakter!'
|
? 'Minimal 5 karakter!'
|
||||||
: username.includes(' ')
|
: username.includes(' ')
|
||||||
? 'Tidak boleh ada spasi'
|
? 'Tidak boleh ada spasi'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -114,29 +108,9 @@ export default function Registrasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box pt="md">
|
<Box pt="md">
|
||||||
<Checkbox
|
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
||||||
checked={agree}
|
|
||||||
onChange={(e) => setAgree(e.currentTarget.checked)}
|
|
||||||
label={
|
|
||||||
<Text fz="sm">
|
|
||||||
Saya menyetujui{" "}
|
|
||||||
<a
|
|
||||||
href="/terms-of-service"
|
|
||||||
target="_blank"
|
|
||||||
style={{
|
|
||||||
color: colors["blue-button"],
|
|
||||||
textDecoration: "underline",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
syarat dan ketentuan
|
|
||||||
</a>
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<Box pt="xl">
|
<Box pt="xl">
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { authStore } from '@/store/authStore';
|
|||||||
|
|
||||||
export default function Validasi() {
|
export default function Validasi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [nomor, setNomor] = useState<string | null>(null);
|
const [nomor, setNomor] = useState<string | null>(null);
|
||||||
const [otp, setOtp] = useState('');
|
const [otp, setOtp] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -31,11 +31,11 @@ export default function Validasi() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFlow = async () => {
|
const checkFlow = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/get-flow', {
|
const res = await fetch('/api/get-flow', {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setIsRegistrationFlow(data.flow === 'register');
|
setIsRegistrationFlow(data.flow === 'register');
|
||||||
console.log('🔍 Flow detected from cookie:', data.flow);
|
console.log('🔍 Flow detected from cookie:', data.flow);
|
||||||
@@ -45,7 +45,7 @@ export default function Validasi() {
|
|||||||
setIsRegistrationFlow(false);
|
setIsRegistrationFlow(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkFlow();
|
checkFlow();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export default function Validasi() {
|
|||||||
setKodeId(storedKodeId);
|
setKodeId(storedKodeId);
|
||||||
const loadOtpData = async () => {
|
const loadOtpData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
const res = await fetch(`/api/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
if (res.ok && result.data?.nomor) {
|
if (res.ok && result.data?.nomor) {
|
||||||
@@ -110,8 +110,7 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Verify OTP
|
const verifyRes = await fetch('/api/verify-otp-register', {
|
||||||
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
||||||
@@ -124,32 +123,26 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Finalize registration
|
const finalizeRes = await fetch('/api/finalize-registration', {
|
||||||
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
|
body: JSON.stringify({ nomor, username, kodeId }),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await finalizeRes.json();
|
const data = await finalizeRes.json();
|
||||||
|
|
||||||
// ✅ Check JSON response (bukan redirect)
|
if (data.success || finalizeRes.redirected) {
|
||||||
if (data.success) {
|
// ✅ Cleanup setelah registrasi sukses
|
||||||
toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
|
|
||||||
await cleanupStorage();
|
await cleanupStorage();
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
// ✅ Client-side redirect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/waiting-room';
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Registrasi gagal');
|
toast.error(data.message || 'Registrasi gagal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginVerification = async () => {
|
const handleLoginVerification = async () => {
|
||||||
const loginRes = await fetch('/api/auth/verify-otp-login', {
|
const loginRes = await fetch('/api/verify-otp-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, otp, kodeId }),
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
@@ -204,10 +197,10 @@ export default function Validasi() {
|
|||||||
localStorage.removeItem('auth_kodeId');
|
localStorage.removeItem('auth_kodeId');
|
||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
localStorage.removeItem('auth_username');
|
localStorage.removeItem('auth_username');
|
||||||
|
|
||||||
// Clear cookie
|
// Clear cookie
|
||||||
try {
|
try {
|
||||||
await fetch('/api/auth/clear-flow', {
|
await fetch('/api/clear-flow', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
@@ -219,7 +212,7 @@ export default function Validasi() {
|
|||||||
const handleResend = async () => {
|
const handleResend = async () => {
|
||||||
if (!nomor) return;
|
if (!nomor) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/resend', {
|
const res = await fetch('/api/resend', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor }),
|
body: JSON.stringify({ nomor }),
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function EditKategoriBerita() {
|
|||||||
{/* Form Wrapper */}
|
{/* Form Wrapper */}
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '50%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* 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';
|
||||||
@@ -8,22 +10,19 @@ 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 { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
// Tipe form yang SESUAI dengan logika relasi (array ID)
|
interface FasilitasKesehatanFormBase {
|
||||||
interface EditFasilitasKesehatanForm {
|
|
||||||
name: string;
|
name: string;
|
||||||
informasiUmum: {
|
informasiUmum: {
|
||||||
fasilitas: string;
|
fasilitas: string;
|
||||||
@@ -31,92 +30,128 @@ interface EditFasilitasKesehatanForm {
|
|||||||
jamOperasional: string;
|
jamOperasional: string;
|
||||||
};
|
};
|
||||||
layananUnggulan: { content: string };
|
layananUnggulan: { content: string };
|
||||||
dokterdanTenagaMedis: string[]; // ← ARRAY ID
|
dokterdanTenagaMedis: {
|
||||||
|
name: string;
|
||||||
|
specialist: string;
|
||||||
|
jadwal: string;
|
||||||
|
};
|
||||||
fasilitasPendukung: { content: string };
|
fasilitasPendukung: { content: string };
|
||||||
prosedurPendaftaran: { content: string };
|
prosedurPendaftaran: { content: string };
|
||||||
tarifDanLayanan: string[]; // ← ARRAY ID
|
tarifDanLayanan: {
|
||||||
|
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<{ id: string }>();
|
const params = useParams();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<EditFasilitasKesehatanForm>({
|
const [formData, setFormData] = useState<FasilitasKesehatanFormBase>({
|
||||||
name: '',
|
name: '',
|
||||||
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
||||||
layananUnggulan: { content: '' },
|
layananUnggulan: { content: '' },
|
||||||
dokterdanTenagaMedis: [],
|
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
|
||||||
fasilitasPendukung: { content: '' },
|
fasilitasPendukung: { content: '' },
|
||||||
prosedurPendaftaran: { content: '' },
|
prosedurPendaftaran: { content: '' },
|
||||||
tarifDanLayanan: [],
|
tarifDanLayanan: { layanan: '', tarif: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load data fasilitas & daftar dokter/tarif
|
const [originalData, setOriginalData] = useState<FasilitasKesehatanFormBase>({
|
||||||
useShallowEffect(() => {
|
name: '',
|
||||||
const loadAll = async () => {
|
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
||||||
const id = params?.id;
|
layananUnggulan: { content: '' },
|
||||||
if (!id) return;
|
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
|
||||||
|
fasilitasPendukung: { content: '' },
|
||||||
|
prosedurPendaftaran: { content: '' },
|
||||||
|
tarifDanLayanan: { layanan: '', tarif: '' },
|
||||||
|
});
|
||||||
|
|
||||||
// Load dokter & tarif (untuk opsi MultiSelect)
|
// Helper untuk update nested state
|
||||||
await Promise.all([
|
const updateForm = <K extends keyof FasilitasKesehatanFormBase>(
|
||||||
dokterState.findMany.load(),
|
key: K,
|
||||||
tarifState.findMany.load(),
|
value: FasilitasKesehatanFormBase[K]
|
||||||
]);
|
) => setFormData(prev => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
// Load data fasilitas
|
const updateNested = <
|
||||||
await state.edit.load(id);
|
K extends keyof FasilitasKesehatanFormBase,
|
||||||
const loaded = state.edit.form;
|
N extends keyof FasilitasKesehatanFormBase[K]
|
||||||
if (loaded) {
|
>(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) =>
|
||||||
setFormData({
|
setFormData(prev => ({
|
||||||
name: loaded.name,
|
...prev,
|
||||||
informasiUmum: loaded.informasiUmum,
|
[key]: { ...prev[key] as object, [nestedKey]: value },
|
||||||
layananUnggulan: loaded.layananUnggulan,
|
}));
|
||||||
dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [],
|
|
||||||
fasilitasPendukung: loaded.fasilitasPendukung,
|
|
||||||
prosedurPendaftaran: loaded.prosedurPendaftaran,
|
|
||||||
tarifDanLayanan: loaded.tarifDanLayanan || [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadAll();
|
const deepClone = (obj: any): any => {
|
||||||
}, [params?.id]);
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
const updateForm = <K extends keyof EditFasilitasKesehatanForm>(
|
} catch (error) {
|
||||||
field: K,
|
console.warn('Gagal deep clone dengan JSON fallback:', error);
|
||||||
value: EditFasilitasKesehatanForm[K]
|
return obj; // fallback (berisiko shared reference)
|
||||||
) => {
|
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
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) => {
|
// Load data
|
||||||
e.preventDefault();
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await state.edit.load(id);
|
||||||
|
const loadedData = state.edit.form;
|
||||||
|
|
||||||
|
if (!loadedData) {
|
||||||
|
toast.error('Data tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gunakan JSON fallback untuk deep clone
|
||||||
|
const clonedData = deepClone(loadedData) as FasilitasKesehatanFormBase;
|
||||||
|
|
||||||
|
setFormData(clonedData);
|
||||||
|
setOriginalData(clonedData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('Gagal memuat data fasilitas kesehatan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
informasiUmum:
|
||||||
|
{
|
||||||
|
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 handleSubmit = async () => {
|
||||||
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!');
|
||||||
@@ -124,14 +159,14 @@ function EditFasilitasKesehatan() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('Gagal memperbarui data');
|
toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* 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">
|
||||||
@@ -154,7 +189,7 @@ function EditFasilitasKesehatan() {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Fasilitas Kesehatan"
|
label="Nama Fasilitas Kesehatan"
|
||||||
placeholder="Masukkan nama"
|
placeholder="Masukkan nama fasilitas kesehatan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => updateForm('name', e.target.value)}
|
onChange={(e) => updateForm('name', e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -162,108 +197,118 @@ function EditFasilitasKesehatan() {
|
|||||||
|
|
||||||
{/* Informasi Umum */}
|
{/* Informasi Umum */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>Informasi Umum</Text>
|
<Text fw="bold" mb={5}>
|
||||||
|
Informasi Umum
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Fasilitas"
|
label="Fasilitas"
|
||||||
value={formData.informasiUmum.fasilitas}
|
value={formData.informasiUmum.fasilitas}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateNested('informasiUmum', 'fasilitas', e.target.value)}
|
||||||
updateForm('informasiUmum', {
|
|
||||||
...formData.informasiUmum,
|
|
||||||
fasilitas: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Alamat"
|
label="Alamat"
|
||||||
value={formData.informasiUmum.alamat}
|
value={formData.informasiUmum.alamat}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateNested('informasiUmum', 'alamat', e.target.value)}
|
||||||
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) =>
|
onChange={(e) => updateNested('informasiUmum', 'jamOperasional', e.target.value)}
|
||||||
updateForm('informasiUmum', {
|
|
||||||
...formData.informasiUmum,
|
|
||||||
jamOperasional: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Layanan Unggulan */}
|
{/* Layanan Unggulan */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>Layanan Unggulan</Text>
|
<Text fw="bold" mb={5}>
|
||||||
|
Layanan Unggulan
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.layananUnggulan.content}
|
value={formData.layananUnggulan.content}
|
||||||
onChange={(v) => updateForm('layananUnggulan', { content: v })}
|
onChange={(v) => updateNested('layananUnggulan', 'content', v)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Dokter & Tenaga Medis — MultiSelect */}
|
{/* Dokter dan Tenaga Medis */}
|
||||||
<MultiSelect
|
<Box>
|
||||||
label="Dokter & Tenaga Medis"
|
<Text fw="bold" mb={5}>
|
||||||
placeholder="Pilih dokter/tenaga medis"
|
Dokter dan Tenaga Medis
|
||||||
data={
|
</Text>
|
||||||
dokterState.findMany.data?.map((d) => ({
|
<TextInput
|
||||||
value: d.id,
|
label="Nama Dokter"
|
||||||
label: `${d.name} (${d.specialist})`,
|
value={formData.dokterdanTenagaMedis.name}
|
||||||
})) || []
|
onChange={(e) => updateNested('dokterdanTenagaMedis', 'name', e.target.value)}
|
||||||
}
|
/>
|
||||||
value={formData.dokterdanTenagaMedis}
|
<TextInput
|
||||||
onChange={(val) => updateForm('dokterdanTenagaMedis', val)}
|
label="Specialist"
|
||||||
searchable
|
value={formData.dokterdanTenagaMedis.specialist}
|
||||||
clearable
|
onChange={(e) =>
|
||||||
required
|
updateNested('dokterdanTenagaMedis', 'specialist', e.target.value)
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
<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}>Fasilitas Pendukung</Text>
|
<Text fw="bold" mb={5}>
|
||||||
|
Fasilitas Pendukung
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.fasilitasPendukung.content}
|
value={formData.fasilitasPendukung.content}
|
||||||
onChange={(v) => updateForm('fasilitasPendukung', { content: v })}
|
onChange={(v) => updateNested('fasilitasPendukung', 'content', v)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Prosedur Pendaftaran */}
|
{/* Prosedur Pendaftaran */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
|
<Text fw="bold" mb={5}>
|
||||||
|
Prosedur Pendaftaran
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.prosedurPendaftaran.content}
|
value={formData.prosedurPendaftaran.content}
|
||||||
onChange={(v) => updateForm('prosedurPendaftaran', { content: v })}
|
onChange={(v) => updateNested('prosedurPendaftaran', 'content', v)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Tarif & Layanan — MultiSelect */}
|
{/* Tarif dan Layanan */}
|
||||||
<MultiSelect
|
<Box>
|
||||||
label="Tarif & Layanan"
|
<Text fw="bold" mb={5}>
|
||||||
placeholder="Pilih layanan"
|
Tarif dan Layanan
|
||||||
data={
|
</Text>
|
||||||
tarifState.findMany.data?.map((t) => ({
|
<TextInput
|
||||||
value: t.id,
|
label="Tarif"
|
||||||
label: `${t.layanan} - ${t.tarif}`,
|
value={formData.tarifDanLayanan.tarif}
|
||||||
})) || []
|
onChange={(e) => updateNested('tarifDanLayanan', 'tarif', e.target.value)}
|
||||||
}
|
/>
|
||||||
value={formData.tarifDanLayanan}
|
<TextInput
|
||||||
onChange={(val) => updateForm('tarifDanLayanan', val)}
|
label="Layanan"
|
||||||
searchable
|
value={formData.tarifDanLayanan.layanan}
|
||||||
clearable
|
onChange={(e) => updateNested('tarifDanLayanan', 'layanan', e.target.value)}
|
||||||
required
|
/>
|
||||||
/>
|
</Box>
|
||||||
|
|
||||||
{/* Aksi */}
|
{/* Tombol Simpan */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button variant="outline" color="gray" radius="md" onClick={handleReset}>
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
onClick={handleSubmit}
|
||||||
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',
|
||||||
@@ -279,4 +324,4 @@ function EditFasilitasKesehatan() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditFasilitasKesehatan;
|
export default EditFasilitasKesehatan;
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ 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';
|
||||||
@@ -114,79 +108,21 @@ function DetailFasilitasKesehatan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold" mb="sm">Dokter & Tenaga Medis</Text>
|
<Text fz="lg" fw="bold">Dokter & Tenaga Medis</Text>
|
||||||
{Array.isArray(data.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? (
|
<Text fz="md" fw="bold">Nama</Text>
|
||||||
<Box style={{ overflowX: 'auto', width: '100%' }}>
|
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.name || '-'}</Text>
|
||||||
<Table striped highlightOnHover withTableBorder>
|
<Text fz="md" fw="bold">Spesialis</Text>
|
||||||
<TableThead>
|
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.specialist || '-'}</Text>
|
||||||
<TableTr>
|
<Text fz="md" fw="bold">Jadwal</Text>
|
||||||
<TableTh style={{ whiteSpace: 'nowrap' }}>Nama</TableTh>
|
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.jadwal || '-'}</Text>
|
||||||
<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 mt="xl">
|
<Box>
|
||||||
<Text fz="lg" fw="bold" mb="sm">Tarif & Layanan</Text>
|
<Text fz="lg" fw="bold">Tarif & Layanan</Text>
|
||||||
{Array.isArray(data.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
|
<Text fz="md" fw="bold">Layanan</Text>
|
||||||
<Box style={{ overflowX: 'auto', width: '100%' }}>
|
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.layanan || '-'}</Text>
|
||||||
<Table striped highlightOnHover withTableBorder>
|
<Text fz="md" fw="bold">Tarif</Text>
|
||||||
<TableThead>
|
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.tarif || '-'}</Text>
|
||||||
<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 */}
|
||||||
|
|||||||
@@ -7,20 +7,19 @@ 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();
|
||||||
@@ -35,16 +34,23 @@ function CreateFasilitasKesehatan() {
|
|||||||
jamOperasional: '',
|
jamOperasional: '',
|
||||||
},
|
},
|
||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: ''
|
content: '',
|
||||||
|
},
|
||||||
|
dokterdanTenagaMedis: {
|
||||||
|
name: '',
|
||||||
|
specialist: '',
|
||||||
|
jadwal: '',
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis: [] as string[],
|
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
tarifDanLayanan: [] as string[],
|
tarifDanLayanan: {
|
||||||
|
layanan: '',
|
||||||
|
tarif: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,11 +70,6 @@ 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 */}
|
||||||
@@ -139,25 +140,31 @@ function CreateFasilitasKesehatan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
{/* Dokter dan Tenaga Medis */}
|
{/* Dokter dan Tenaga Medis */}
|
||||||
<MultiSelect
|
<Box>
|
||||||
label="Dokter & Tenaga Medis"
|
<Text fz="md" fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
|
||||||
placeholder="Pilih dokter / tenaga medis"
|
<TextInput
|
||||||
data={
|
label="Nama Dokter"
|
||||||
fasilitasKesehatanState.dokter.findMany.data?.map((item) => ({
|
placeholder="Masukkan nama dokter"
|
||||||
label: item.name,
|
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
|
||||||
value: item.id,
|
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)}
|
||||||
})) || []
|
required
|
||||||
}
|
/>
|
||||||
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis}
|
<TextInput
|
||||||
onChange={(val: string[]) => {
|
label="Spesialis"
|
||||||
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis = val;
|
placeholder="Masukkan spesialis"
|
||||||
}}
|
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
|
||||||
searchable
|
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
|
||||||
clearable
|
required
|
||||||
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>
|
||||||
@@ -168,24 +175,6 @@ 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>
|
||||||
@@ -195,6 +184,24 @@ 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">
|
||||||
|
|||||||
@@ -1,241 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import React from 'react';
|
||||||
/* 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 (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<div>
|
||||||
{/* Header */}
|
Page
|
||||||
<Group mb="md">
|
</div>
|
||||||
<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 EditDokterTenagaMedis;
|
export default Page;
|
||||||
|
|||||||
@@ -1,165 +1,11 @@
|
|||||||
'use client'
|
import React from 'react';
|
||||||
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 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 (
|
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton height={500} radius="md" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = state.findUnique.data;
|
|
||||||
|
|
||||||
|
function Page() {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<div>
|
||||||
{/* Tombol Back */}
|
Page
|
||||||
<Button
|
</div>
|
||||||
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;
|
export default Page;
|
||||||
|
|||||||
@@ -1,184 +1,71 @@
|
|||||||
/* 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 { ActionIcon, Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { TimeInput } from '@mantine/dates';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { IconArrowBack, IconClock } from '@tabler/icons-react';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
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 {
|
await createState.create.create.create();
|
||||||
setIsSubmitting(true);
|
resetForm();
|
||||||
await createState.create.create.create();
|
router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`)
|
||||||
toast.success('Data berhasil disimpan');
|
|
||||||
resetForm();
|
|
||||||
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 px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
<Box mb={10}>
|
||||||
<Group mb="md">
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
<Button
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
variant="subtle"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
p="xs"
|
|
||||||
radius="md"
|
|
||||||
>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
</Box>
|
||||||
Tambah Data Dokter & Tenaga Medis
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Form */}
|
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
||||||
<Paper
|
<Stack gap="xs">
|
||||||
w={{ base: '100%', md: '50%' }}
|
<Title order={3}>Create Dokter</Title>
|
||||||
bg={colors['white-1']}
|
|
||||||
p="lg"
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
style={{ border: '1px solid #e0e0e0' }}
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={"Nama Dokter"}
|
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
|
||||||
placeholder="Masukkan nama dokter"
|
placeholder="masukkan nama dokter"
|
||||||
value={createState.create.create.form.name}
|
value={createState.create.create.form.name}
|
||||||
onChange={(e) => (createState.create.create.form.name = e.target.value)}
|
onChange={(e) => {
|
||||||
required
|
createState.create.create.form.name = e.target.value;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Text fz="md" fw="bold">Specialist</Text>
|
||||||
{/* Informasi Umum */}
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Specialist"
|
label={<Text fz="sm" fw="bold">Specialist</Text>}
|
||||||
placeholder="Masukkan specialist"
|
placeholder="masukkan specialist"
|
||||||
value={createState.create.create.form.specialist}
|
value={createState.create.create.form.specialist}
|
||||||
onChange={(e) => (createState.create.create.form.specialist = e.target.value)}
|
onChange={(e) => {
|
||||||
required
|
createState.create.create.form.specialist = e.target.value;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Box>
|
||||||
<TextInput
|
<Text fz="md" fw="bold">Jadwal</Text>
|
||||||
label="Jadwal"
|
<CreateEditor
|
||||||
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
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* 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'}
|
</Box>
|
||||||
</Button>
|
<Button onClick={handleSubmit} bg={colors['blue-button']}>
|
||||||
</Group>
|
Simpan
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconArrowBack, IconDeviceImacCog, 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';
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ function DokterTenagaMedis() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -59,101 +60,49 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
{/* Judul + Tombol Tambah */}
|
<Stack>
|
||||||
<Group justify="space-between" mb="md">
|
<JudulList
|
||||||
<Title order={4}>Daftar Dokter dan Tenaga Medis</Title>
|
title='List Fasilitas Kesehatan'
|
||||||
<Button
|
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`}
|
||||||
leftSection={<IconPlus size={18} />}
|
/>
|
||||||
color="blue"
|
<Box style={{ overflowX: "auto" }}>
|
||||||
variant="light"
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
onClick={() =>
|
<TableThead>
|
||||||
router.push(
|
<TableTr>
|
||||||
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create'
|
<TableTh>Fasilitas Kesehatan</TableTh>
|
||||||
)
|
<TableTh>Alamat</TableTh>
|
||||||
}
|
<TableTh>Jam Operasional</TableTh>
|
||||||
>
|
<TableTh>Detail</TableTh>
|
||||||
Tambah Baru
|
</TableTr>
|
||||||
</Button>
|
</TableThead>
|
||||||
</Group>
|
<TableTbody>
|
||||||
|
{filteredData.map((item) => (
|
||||||
{/* Tabel */}
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table highlightOnHover>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Nama Dokter</TableTh>
|
|
||||||
<TableTh>Spesialis</TableTh>
|
|
||||||
<TableTh>Jadwal</TableTh>
|
|
||||||
<TableTh>Aksi</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.length > 0 ? (
|
|
||||||
filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>{item.specialist}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} />
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
|
||||||
{item.specialist || '-'}
|
<IconDeviceImacCog size={25} />
|
||||||
</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>
|
||||||
))
|
))}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={4}>
|
</Box>
|
||||||
<Center py={20}>
|
</Stack>
|
||||||
<Text c="dimmed">
|
|
||||||
Tidak ada fasilitas kesehatan yang cocok
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => load(newPage)} // ini penting!
|
||||||
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>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
'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,
|
||||||
@@ -19,52 +16,30 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
Title
|
||||||
Title,
|
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, 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>
|
||||||
<Grid mb={10}>
|
{/* Header Search */}
|
||||||
<GridCol span={{ base: 12, md: 8 }}>
|
<HeaderSearch
|
||||||
<Title order={3}>Fasilitas Kesehatan</Title>
|
title='Fasilitas Kesehatan'
|
||||||
</GridCol>
|
placeholder='Cari nama, alamat, atau jam operasional...'
|
||||||
<GridCol span={{ base: 12, md: 4 }}>
|
searchIcon={<IconSearch size={20} />}
|
||||||
<Group gap={"xs"}>
|
value={search}
|
||||||
<Tooltip label="List Dokter" withArrow>
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
<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...'
|
|
||||||
leftSection={<IconSearch size={20} />}
|
|
||||||
w="133%"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Group>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ListFasilitasKesehatan search={search} />
|
<ListFasilitasKesehatan search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -79,7 +54,6 @@ 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]);
|
||||||
|
|
||||||
@@ -119,8 +93,8 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Fasilitas Kesehatan</TableTh>
|
<TableTh>Fasilitas Kesehatan</TableTh>
|
||||||
<TableTh>Jumlah Dokter</TableTh>
|
<TableTh>Dokter</TableTh>
|
||||||
<TableTh>Jumlah Layanan</TableTh>
|
<TableTh>Layanan</TableTh>
|
||||||
<TableTh>Aksi</TableTh>
|
<TableTh>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
@@ -137,17 +111,13 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Box w={150}>
|
||||||
{item.dokterdantenagamedis?.length
|
{item.dokterdantenagamedis?.name || '-'}
|
||||||
? `${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?.length
|
{item.tarifdanlayanan?.layanan || '-'}
|
||||||
? `${item.tarifdanlayanan.length} layanan`
|
|
||||||
: '-'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -171,7 +141,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTd colSpan={4}>
|
||||||
<Center py={20}>
|
<Center py={20}>
|
||||||
<Text c="dimmed">
|
<Text color="dimmed">
|
||||||
Tidak ada fasilitas kesehatan yang cocok
|
Tidak ada fasilitas kesehatan yang cocok
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
'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;
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconDeviceImacCog, 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 { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
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,12 +18,12 @@ function TarifLayanan() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Tarif dan Layanan'
|
title='Dokter dan Tenaga Medis'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -35,11 +35,8 @@ function TarifLayanan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListTarifLayanan({ search }: { search: string }) {
|
function ListTarifLayanan({ search }: { search: string }) {
|
||||||
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.tarif);
|
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
@@ -52,15 +49,6 @@ 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) {
|
||||||
@@ -72,116 +60,51 @@ function ListTarifLayanan({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
{/* Judul + Tombol Tambah */}
|
<Stack>
|
||||||
<Group justify="space-between" mb="md">
|
<JudulList
|
||||||
<Title order={4}>Daftar Tarif dan Layanan</Title>
|
title='List Fasilitas Kesehatan'
|
||||||
<Button
|
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`}
|
||||||
leftSection={<IconPlus size={18} />}
|
/>
|
||||||
color="blue"
|
<Box style={{ overflowX: "auto" }}>
|
||||||
variant="light"
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
onClick={() =>
|
<TableThead>
|
||||||
router.push(
|
<TableTr>
|
||||||
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create'
|
<TableTh>Fasilitas Kesehatan</TableTh>
|
||||||
)
|
<TableTh>Alamat</TableTh>
|
||||||
}
|
<TableTh>Jam Operasional</TableTh>
|
||||||
>
|
<TableTh>Detail</TableTh>
|
||||||
Tambah Baru
|
</TableTr>
|
||||||
</Button>
|
</TableThead>
|
||||||
</Group>
|
<TableTbody>
|
||||||
|
{filteredData.map((item) => (
|
||||||
{/* Tabel */}
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table highlightOnHover>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Layanan</TableTh>
|
|
||||||
<TableTh>Tarif</TableTh>
|
|
||||||
<TableTh>Edit</TableTh>
|
|
||||||
<TableTh>Hapus</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.length > 0 ? (
|
|
||||||
filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>{item.specialist}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} />
|
||||||
{item.layanan || '-'}
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
<IconDeviceImacCog size={25} />
|
||||||
{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>
|
||||||
))
|
))}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={4}>
|
</Box>
|
||||||
<Center py={20}>
|
</Stack>
|
||||||
<Text c="dimmed">
|
|
||||||
Tidak ada fasilitas kesehatan yang cocok
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => load(newPage)} // ini penting!
|
||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,7 +361,6 @@ function CreateAPBDes() {
|
|||||||
data={[
|
data={[
|
||||||
{ value: 'pendapatan', label: 'Pendapatan' },
|
{ value: 'pendapatan', label: 'Pendapatan' },
|
||||||
{ value: 'belanja', label: 'Belanja' },
|
{ value: 'belanja', label: 'Belanja' },
|
||||||
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
|
||||||
]}
|
]}
|
||||||
value={newItem.level === 1 ? null : newItem.tipe}
|
value={newItem.level === 1 ? null : newItem.tipe}
|
||||||
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import SelectSocialMediaEdit from '@/app/admin/(dashboard)/_com/selectSocialMediaEdit';
|
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
@@ -16,7 +14,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Loader,
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
@@ -25,45 +23,15 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
type SosmedKey =
|
|
||||||
| 'none'
|
|
||||||
| 'facebook'
|
|
||||||
| 'instagram'
|
|
||||||
| 'tiktok'
|
|
||||||
| 'youtube'
|
|
||||||
| 'whatsapp'
|
|
||||||
| 'gmail'
|
|
||||||
| 'telegram'
|
|
||||||
| 'x'
|
|
||||||
| 'telephone'
|
|
||||||
| 'custom';
|
|
||||||
|
|
||||||
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
|
||||||
none: { label: "None", src: '/no-image.jpg' },
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditMediaSosial() {
|
function EditMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
icon: '',
|
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
});
|
});
|
||||||
@@ -71,14 +39,13 @@ function EditMediaSosial() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
icon: '',
|
iconUrl: "",
|
||||||
iconUrl: '',
|
imageId: "",
|
||||||
imageId: '',
|
imageUrl: "",
|
||||||
imageUrl: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load Data by ID
|
// Load data by ID
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -87,97 +54,81 @@ function EditMediaSosial() {
|
|||||||
try {
|
try {
|
||||||
const data = await stateMediaSosial.update.load(id);
|
const data = await stateMediaSosial.update.load(id);
|
||||||
|
|
||||||
if (!data) return;
|
if (data) {
|
||||||
|
// isi form awal
|
||||||
|
const newForm = {
|
||||||
|
name: data.name || "",
|
||||||
|
iconUrl: data.iconUrl || "",
|
||||||
|
imageId: data.imageId || "",
|
||||||
|
};
|
||||||
|
setFormData(newForm);
|
||||||
|
|
||||||
// Tentukan default/custom icon
|
// simpan juga versi original
|
||||||
// Tentukan default/custom icon
|
setOriginalData({
|
||||||
if (data.imageId) {
|
...newForm,
|
||||||
setSelectedSosmed('custom');
|
imageUrl: data.image?.link || "",
|
||||||
} else {
|
});
|
||||||
// ✅ Gunakan langsung data.icon jika ada dan valid
|
|
||||||
if (data.icon && sosmedMap[data.icon as SosmedKey]) {
|
setPreviewImage(data.image?.link || null);
|
||||||
setSelectedSosmed(data.icon as SosmedKey);
|
|
||||||
} else {
|
|
||||||
setSelectedSosmed('none'); // fallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
const newForm = {
|
console.error('Error loading media sosial:', error);
|
||||||
name: data.name || '',
|
toast.error(
|
||||||
icon: data.icon || '',
|
error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
|
||||||
iconUrl: data.iconUrl || '',
|
);
|
||||||
imageId: data.imageId || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData(newForm);
|
|
||||||
|
|
||||||
setOriginalData({
|
|
||||||
...newForm,
|
|
||||||
imageUrl: data.image?.link || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
setPreviewImage(data.image?.link || null);
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal mengambil data media sosial');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleChange = (field: keyof typeof formData, value: string) => {
|
const handleChange = (field: string, value: string) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// update global state hanya saat submit
|
||||||
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
toast.error('Gagal upload gambar');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateMediaSosial.update.form.imageId = uploaded.id;
|
stateMediaSosial.update.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚨 Tambahkan ini untuk debugging
|
|
||||||
console.log("Data yang akan dikirim ke backend:", stateMediaSosial.update.form);
|
|
||||||
|
|
||||||
await stateMediaSosial.update.update();
|
await stateMediaSosial.update.update();
|
||||||
toast.success('Media sosial berhasil diperbarui!');
|
toast.success('Media sosial berhasil diperbarui!');
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
router.push('/admin/landing-page/profil/media-sosial');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error di handleSubmit:", error); // 🚨 Tambahkan ini juga
|
console.error('Error updating media sosial:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Tombol Batal → balikin ke data original
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: originalData.name,
|
name: originalData.name,
|
||||||
icon: originalData.icon,
|
|
||||||
iconUrl: originalData.iconUrl,
|
iconUrl: originalData.iconUrl,
|
||||||
imageId: originalData.imageId,
|
imageId: originalData.imageId,
|
||||||
});
|
});
|
||||||
setPreviewImage(originalData.imageUrl || null);
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
toast.info('Form dikembalikan ke data awal');
|
toast.info("Form dikembalikan ke data awal");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box
|
||||||
|
px={{ base: 'sm', md: 'lg' }}
|
||||||
|
py="md"
|
||||||
|
>
|
||||||
<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">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -196,119 +147,80 @@ function EditMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Upload / Icon */}
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Icon / Gambar Media Sosial
|
Gambar Program Inovasi
|
||||||
</Text>
|
</Text>
|
||||||
|
<Dropzone
|
||||||
{/* Custom Upload */}
|
onDrop={(files) => {
|
||||||
{/* PILIH ICON */}
|
const selectedFile = files[0];
|
||||||
<SelectSocialMediaEdit
|
if (selectedFile) {
|
||||||
value={selectedSosmed}
|
setFile(selectedFile);
|
||||||
onChange={(key) => {
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
setSelectedSosmed(key);
|
|
||||||
|
|
||||||
if (key === 'custom') {
|
|
||||||
// custom → gunakan Dropzone
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
icon: '',
|
|
||||||
imageId: '',
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// default → pakai icon bawaan
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
icon: key, // <-- simpan 'facebook', bukan path
|
|
||||||
imageId: '',
|
|
||||||
}));
|
|
||||||
}}
|
}}
|
||||||
/>
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
{selectedSosmed === 'custom' ? (
|
{/* ✅ Preview gambar + tombol X */}
|
||||||
<>
|
{previewImage && (
|
||||||
<Dropzone
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
onDrop={(files) => {
|
|
||||||
const selectedFile = files[0];
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
|
||||||
handleChange('imageId', '');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid')}
|
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
|
||||||
radius="md"
|
|
||||||
p="xl"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
|
|
||||||
<Stack align="center" gap="xs">
|
|
||||||
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format: .png, .jpg, .jpeg, .webp
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 200,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionIcon
|
|
||||||
variant="filled"
|
|
||||||
color="red"
|
|
||||||
radius="xl"
|
|
||||||
size="sm"
|
|
||||||
pos="absolute"
|
|
||||||
top={5}
|
|
||||||
right={5}
|
|
||||||
onClick={() => {
|
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
|
||||||
handleChange('imageId', '');
|
|
||||||
}}
|
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
|
||||||
>
|
|
||||||
<IconX size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Default icon
|
|
||||||
<Box mt="xs">
|
|
||||||
<Image
|
<Image
|
||||||
src={sosmedMap[selectedSosmed].src || ''}
|
src={previewImage}
|
||||||
alt="Icon bawaan"
|
alt="Preview Gambar"
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{ border: '1px solid #ddd', padding: 4, background: '#fff' }}
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -325,17 +237,25 @@ function EditMediaSosial() {
|
|||||||
{/* Link Media Sosial */}
|
{/* Link Media Sosial */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link Media Sosial / Nomor Telepon"
|
label="Link Media Sosial / Nomor Telepon"
|
||||||
placeholder="Masukkan link atau nomor telepon"
|
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||||
value={formData.iconUrl}
|
value={formData.iconUrl}
|
||||||
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={handleResetForm}>
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
'use client'
|
'use client'
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
@@ -9,7 +8,6 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { sosmedMap } from '../../_lib/sosmed';
|
|
||||||
|
|
||||||
function DetailMediaSosial() {
|
function DetailMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
@@ -18,14 +16,6 @@ function DetailMediaSosial() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
|
||||||
if (item.image?.link) return item.image.link;
|
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateMediaSosial.findUnique.load(params?.id as string);
|
stateMediaSosial.findUnique.load(params?.id as string);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -87,47 +77,46 @@ function DetailMediaSosial() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Gambar</Text>
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
{(() => {
|
{data.image?.link ? (
|
||||||
const src = getIconSource(data);
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name || 'Gambar Media Sosial'}
|
||||||
|
w="100%"
|
||||||
|
maw={120} // max width biar tidak keluar layar
|
||||||
|
h="auto"
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
if (src) {
|
) : (
|
||||||
return (
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
<Image
|
)}
|
||||||
loading="lazy"
|
|
||||||
src={src}
|
|
||||||
alt={data.name}
|
|
||||||
fit={data.image?.link ? "cover" : "contain"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
|
||||||
})()}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(data.id);
|
setSelectedId(data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${data.id}/edit`)}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import {
|
import {
|
||||||
@@ -23,41 +22,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||||
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
|
||||||
|
|
||||||
|
|
||||||
// ⭐ Tambah type SosmedKey
|
|
||||||
type SosmedKey =
|
|
||||||
| 'facebook'
|
|
||||||
| 'instagram'
|
|
||||||
| 'tiktok'
|
|
||||||
| 'youtube'
|
|
||||||
| 'whatsapp'
|
|
||||||
| 'gmail'
|
|
||||||
| 'telegram'
|
|
||||||
| 'x'
|
|
||||||
| 'telephone'
|
|
||||||
| 'custom';
|
|
||||||
|
|
||||||
// ⭐ mapping icon sosmed bawaan
|
|
||||||
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CreateMediaSosial() {
|
export default function CreateMediaSosial() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
|
|
||||||
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -71,34 +39,16 @@ export default function CreateMediaSosial() {
|
|||||||
name: '',
|
name: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
icon: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setSelectedSosmed('facebook');
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
|
|
||||||
if (selectedSosmed !== 'custom') {
|
|
||||||
stateMediaSosial.create.form.imageId = null;
|
|
||||||
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
|
||||||
|
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────── ⭐ CASE 2: CUSTOM ICON → WAJIB UPLOAD ────────────────
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
toast.warn('Silakan upload icon custom terlebih dahulu');
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
@@ -109,12 +59,10 @@ export default function CreateMediaSosial() {
|
|||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
toast.error('Gagal mengunggah icon custom');
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stateMediaSosial.create.form.imageId = uploaded.id;
|
stateMediaSosial.create.form.imageId = uploaded.id;
|
||||||
stateMediaSosial.create.form.icon = null;
|
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
await stateMediaSosial.create.create();
|
||||||
|
|
||||||
@@ -130,7 +78,6 @@ export default function CreateMediaSosial() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* 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">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -149,110 +96,112 @@ export default function CreateMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Select Sosmed */}
|
<Box>
|
||||||
<SelectSosialMedia value={selectedSosmed} onChange={setSelectedSosmed} />
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Program Inovasi
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
{/* Custom icon uploader */}
|
{/* ✅ Preview gambar + tombol X */}
|
||||||
{selectedSosmed === 'custom' && (
|
{previewImage && (
|
||||||
<Box>
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Image
|
||||||
Upload Custom Icon
|
src={previewImage}
|
||||||
</Text>
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
<Dropzone
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
onDrop={(files) => {
|
<ActionIcon
|
||||||
const selectedFile = files[0];
|
variant="filled"
|
||||||
if (selectedFile) {
|
color="red"
|
||||||
setFile(selectedFile);
|
radius="xl"
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
size="sm"
|
||||||
}
|
pos="absolute"
|
||||||
}}
|
top={5}
|
||||||
onReject={() => toast.error('File tidak valid')}
|
right={5}
|
||||||
maxSize={5 * 1024 ** 2}
|
onClick={() => {
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
setPreviewImage(null);
|
||||||
radius="md"
|
setFile(null);
|
||||||
p="xl"
|
}}
|
||||||
>
|
style={{
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
<Dropzone.Accept>
|
}}
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<IconX size={14} />
|
||||||
<Dropzone.Reject>
|
</ActionIcon>
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
</Box>
|
||||||
</Dropzone.Reject>
|
)}
|
||||||
<Dropzone.Idle>
|
</Box>
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
|
|
||||||
<Stack align="center" gap="xs">
|
|
||||||
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format .png, .jpg, .jpeg, webp
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 200,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionIcon
|
|
||||||
variant="filled"
|
|
||||||
color="red"
|
|
||||||
radius="xl"
|
|
||||||
size="sm"
|
|
||||||
pos="absolute"
|
|
||||||
top={5}
|
|
||||||
right={5}
|
|
||||||
onClick={() => {
|
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
|
||||||
}}
|
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
|
||||||
>
|
|
||||||
<IconX size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Input name */}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Media Sosial"
|
label="Nama Media Sosial / Kontak"
|
||||||
placeholder="Masukkan nama media sosial"
|
placeholder="Masukkan nama media sosial atau kontak"
|
||||||
value={stateMediaSosial.create.form.name ?? ''}
|
value={stateMediaSosial.create.form.name || ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Input link */}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link / Kontak"
|
label="Link Media Sosial / Nomor Telepon"
|
||||||
placeholder="Masukkan link atau nomor"
|
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||||
value={stateMediaSosial.create.form.iconUrl ?? ''}
|
value={stateMediaSosial.create.form.iconUrl || ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button variant="outline" color="gray" radius="md" onClick={resetForm}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
radius="md"
|
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
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',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
@@ -9,7 +8,6 @@ import { useState } from 'react';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||||
import { sosmedMap } from '../_lib/sosmed';
|
|
||||||
|
|
||||||
function MediaSosial() {
|
function MediaSosial() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -31,14 +29,6 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
|
||||||
if (item.image?.link) return item.image.link;
|
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
@@ -66,9 +56,9 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Media Sosial</Title>
|
<Title order={4}>Daftar Media Sosial</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}>
|
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
@@ -87,26 +77,13 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
<TableTd style={{ width: '25%', }}>
|
<TableTd style={{ width: '25%', }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%', }}>
|
||||||
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
|
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden', }}>
|
||||||
|
{item.image?.link ? (
|
||||||
{(() => {
|
<Image loading='lazy' src={item.image.link} alt={item.name} fit="cover" />
|
||||||
const src = getIconSource(item);
|
) : (
|
||||||
|
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||||
if (src) {
|
)}
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
loading="lazy"
|
|
||||||
src={src}
|
|
||||||
alt={item.name}
|
|
||||||
fit={item.image?.link ? "cover" : "contain"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTd style={{ width: '20%', }}>
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ function ListUser({ search }: { search: string }) {
|
|||||||
const success = await stateUser.update.submit({
|
const success = await stateUser.update.submit({
|
||||||
id: userId,
|
id: userId,
|
||||||
roleId: newRoleId,
|
roleId: newRoleId,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -137,10 +136,9 @@ function ListUser({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredData = (data || []).filter((item) => {
|
const filteredData = (data || []).filter(
|
||||||
return item.roleId !== "0" && item.roleId !== "1";
|
(item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
|
||||||
});
|
);
|
||||||
|
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -185,7 +183,7 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<Select
|
<Select
|
||||||
placeholder="Pilih role"
|
placeholder="Pilih role"
|
||||||
data={stateRole.findMany.data
|
data={stateRole.findMany.data
|
||||||
.filter(r => r.id !== "0" && r.id !== "1") // ❌ Sembunyikan SUPERADMIN dan DEVELOPER
|
.filter(r => r.id !== "0") // ❌ Sembunyikan SUPERADMIN
|
||||||
.map(r => ({
|
.map(r => ({
|
||||||
label: r.name,
|
label: r.name,
|
||||||
value: r.id,
|
value: r.id,
|
||||||
|
|||||||
@@ -1,399 +1,3 @@
|
|||||||
// 'use client'
|
|
||||||
|
|
||||||
// import colors from "@/con/colors";
|
|
||||||
// import { authStore } from "@/store/authStore";
|
|
||||||
// import {
|
|
||||||
// ActionIcon,
|
|
||||||
// AppShell,
|
|
||||||
// AppShellHeader,
|
|
||||||
// AppShellMain,
|
|
||||||
// AppShellNavbar,
|
|
||||||
// Burger,
|
|
||||||
// Center,
|
|
||||||
// Flex,
|
|
||||||
// Group,
|
|
||||||
// Image,
|
|
||||||
// Loader,
|
|
||||||
// NavLink,
|
|
||||||
// ScrollArea,
|
|
||||||
// Text,
|
|
||||||
// Tooltip,
|
|
||||||
// rem
|
|
||||||
// } from "@mantine/core";
|
|
||||||
// import { useDisclosure } from "@mantine/hooks";
|
|
||||||
// import {
|
|
||||||
// IconChevronLeft,
|
|
||||||
// IconChevronRight,
|
|
||||||
// IconLogout2
|
|
||||||
// } from "@tabler/icons-react";
|
|
||||||
// import _ from "lodash";
|
|
||||||
// import Link from "next/link";
|
|
||||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
|
||||||
// import { useEffect, useState } from "react";
|
|
||||||
// // import { useSnapshot } from "valtio";
|
|
||||||
// import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
|
||||||
|
|
||||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
|
||||||
// const [opened, { toggle }] = useDisclosure();
|
|
||||||
// const [loading, setLoading] = useState(true);
|
|
||||||
// const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
||||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
|
||||||
// const router = useRouter();
|
|
||||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
|
||||||
|
|
||||||
// // const { user } = useSnapshot(authStore);
|
|
||||||
|
|
||||||
// // console.log("Current user in store:", user);
|
|
||||||
|
|
||||||
// // ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchUser = async () => {
|
|
||||||
// try {
|
|
||||||
// const res = await fetch('/api/auth/me');
|
|
||||||
// const data = await res.json();
|
|
||||||
|
|
||||||
// if (data.user) {
|
|
||||||
// // ✅ Check if user is NOT active → redirect to waiting room
|
|
||||||
// if (!data.user.isActive) {
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/waiting-room');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ✅ Fetch menuIds
|
|
||||||
// const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
|
||||||
// const menuData = await menuRes.json();
|
|
||||||
|
|
||||||
// const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
|
||||||
// ? [...menuData.menuIds]
|
|
||||||
// : null;
|
|
||||||
|
|
||||||
// // ✅ Set user dengan menuIds yang fresh
|
|
||||||
// authStore.setUser({
|
|
||||||
// id: data.user.id,
|
|
||||||
// name: data.user.name,
|
|
||||||
// roleId: Number(data.user.roleId),
|
|
||||||
// menuIds,
|
|
||||||
// isActive: data.user.isActive
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // ✅ TAMBAHKAN INI: Redirect ke dashboard sesuai roleId
|
|
||||||
// const currentPath = window.location.pathname;
|
|
||||||
// const expectedPath = getRedirectPath(Number(data.user.roleId));
|
|
||||||
|
|
||||||
// // Jika user di halaman /admin tapi bukan di path yang sesuai roleId
|
|
||||||
// if (currentPath === '/admin' || !currentPath.startsWith(expectedPath)) {
|
|
||||||
// router.replace(expectedPath);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// } else {
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/login');
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Gagal memuat data pengguna:', error);
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// router.replace('/login');
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetchUser();
|
|
||||||
// }, [router]);
|
|
||||||
|
|
||||||
// // ✅ Fungsi helper untuk get redirect path
|
|
||||||
// const getRedirectPath = (roleId: number): string => {
|
|
||||||
// switch (roleId) {
|
|
||||||
// case 0: // DEVELOPER
|
|
||||||
// case 1: // SUPERADMIN
|
|
||||||
// case 2: // ADMIN_DESA
|
|
||||||
// return '/admin/landing-page/profil/program-inovasi';
|
|
||||||
// case 3: // ADMIN_KESEHATAN
|
|
||||||
// return '/admin/kesehatan/posyandu';
|
|
||||||
// case 4: // ADMIN_PENDIDIKAN
|
|
||||||
// return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
|
||||||
// default:
|
|
||||||
// return '/admin';
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (loading) {
|
|
||||||
// return (
|
|
||||||
// <AppShell>
|
|
||||||
// <AppShellMain>
|
|
||||||
// <Center h="100vh">
|
|
||||||
// <Loader />
|
|
||||||
// </Center>
|
|
||||||
// </AppShellMain>
|
|
||||||
// </AppShell>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ✅ Ambil menu berdasarkan roleId dan menuIds
|
|
||||||
// const currentNav = authStore.user
|
|
||||||
// ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
|
||||||
// : [];
|
|
||||||
|
|
||||||
// const handleLogout = async () => {
|
|
||||||
// try {
|
|
||||||
// setIsLoggingOut(true);
|
|
||||||
|
|
||||||
// // ✅ Panggil API logout untuk clear session di server
|
|
||||||
// const response = await fetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
// const result = await response.json();
|
|
||||||
|
|
||||||
// if (result.success) {
|
|
||||||
// // Clear user data dari store
|
|
||||||
// authStore.setUser(null);
|
|
||||||
|
|
||||||
// // Clear localStorage
|
|
||||||
// localStorage.removeItem('auth_nomor');
|
|
||||||
// localStorage.removeItem('auth_kodeId');
|
|
||||||
|
|
||||||
// // Force reload untuk reset semua state
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// } else {
|
|
||||||
// console.error('Logout failed:', result.message);
|
|
||||||
// // Tetap redirect meskipun gagal
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error during logout:', error);
|
|
||||||
// // Tetap clear store dan redirect jika error
|
|
||||||
// authStore.setUser(null);
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// } finally {
|
|
||||||
// setIsLoggingOut(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <AppShell
|
|
||||||
// suppressHydrationWarning
|
|
||||||
// header={{ height: 64 }}
|
|
||||||
// navbar={{
|
|
||||||
// width: { base: 260, sm: 280, lg: 300 },
|
|
||||||
// breakpoint: 'sm',
|
|
||||||
// collapsed: {
|
|
||||||
// mobile: !opened,
|
|
||||||
// desktop: !desktopOpened,
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// padding="md"
|
|
||||||
// >
|
|
||||||
// <AppShellHeader
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
|
||||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// padding: '0 16px',
|
|
||||||
// }}
|
|
||||||
// px={{ base: 'sm', sm: 'md' }}
|
|
||||||
// py={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
|
||||||
// <Flex align="center" gap="sm">
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={{ base: 32, sm: 40 }}
|
|
||||||
// h={{ base: 32, sm: 40 }}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '32px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <Text
|
|
||||||
// fw={700}
|
|
||||||
// c={colors["blue-button"]}
|
|
||||||
// fz={{ base: 'md', sm: 'xl' }}
|
|
||||||
// >
|
|
||||||
// Admin Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// </Flex>
|
|
||||||
|
|
||||||
// <Group gap="xs">
|
|
||||||
// {!desktopOpened && (
|
|
||||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronRight />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <Burger
|
|
||||||
// opened={opened}
|
|
||||||
// onClick={toggle}
|
|
||||||
// hiddenFrom="sm"
|
|
||||||
// size="md"
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// mr="xs"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={() => {
|
|
||||||
// router.push("/darmasaba");
|
|
||||||
// }}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// >
|
|
||||||
// <Image
|
|
||||||
// src="/assets/images/darmasaba-icon.png"
|
|
||||||
// alt="Logo Darmasaba"
|
|
||||||
// w={20}
|
|
||||||
// h={20}
|
|
||||||
// radius="md"
|
|
||||||
// loading="lazy"
|
|
||||||
// style={{
|
|
||||||
// minWidth: '20px',
|
|
||||||
// height: 'auto',
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
|
||||||
// <ActionIcon
|
|
||||||
// onClick={handleLogout}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// variant="gradient"
|
|
||||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
|
||||||
// loading={isLoggingOut}
|
|
||||||
// disabled={isLoggingOut}
|
|
||||||
// >
|
|
||||||
// <IconLogout2 size={22} />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </Group>
|
|
||||||
// </AppShellHeader>
|
|
||||||
|
|
||||||
// <AppShellNavbar
|
|
||||||
// component={ScrollArea}
|
|
||||||
// style={{
|
|
||||||
// background: "#ffffff",
|
|
||||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
|
||||||
// }}
|
|
||||||
// p={{ base: 'xs', sm: 'sm' }}
|
|
||||||
// >
|
|
||||||
// <AppShell.Section p="sm">
|
|
||||||
// {currentNav.map((v, k) => {
|
|
||||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={k}
|
|
||||||
// defaultOpened={isParentActive}
|
|
||||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
|
||||||
// {v.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// style={{
|
|
||||||
// borderRadius: rem(10),
|
|
||||||
// marginBottom: rem(4),
|
|
||||||
// transition: "background 150ms ease",
|
|
||||||
// }}
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// variant="light"
|
|
||||||
// active={isParentActive}
|
|
||||||
// >
|
|
||||||
// {v.children.map((child, key) => {
|
|
||||||
// const isChildActive = segments.includes(
|
|
||||||
// _.lowerCase(child.name)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <NavLink
|
|
||||||
// key={key}
|
|
||||||
// href={child.path}
|
|
||||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
|
||||||
// label={
|
|
||||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
|
||||||
// {child.name}
|
|
||||||
// </Text>
|
|
||||||
// }
|
|
||||||
// styles={{
|
|
||||||
// root: {
|
|
||||||
// borderRadius: rem(8),
|
|
||||||
// marginBottom: rem(2),
|
|
||||||
// transition: 'background 150ms ease',
|
|
||||||
// padding: '6px 12px',
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
|
||||||
// },
|
|
||||||
// ...(isChildActive && {
|
|
||||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// active={isChildActive}
|
|
||||||
// component={Link}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </NavLink>
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </AppShell.Section>
|
|
||||||
|
|
||||||
// <AppShell.Section py="md">
|
|
||||||
// <Group justify="end" pr="sm">
|
|
||||||
// <Tooltip
|
|
||||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
|
||||||
// position="top"
|
|
||||||
// withArrow
|
|
||||||
// >
|
|
||||||
// <ActionIcon
|
|
||||||
// variant="light"
|
|
||||||
// radius="xl"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={toggleDesktop}
|
|
||||||
// color={colors["blue-button"]}
|
|
||||||
// >
|
|
||||||
// <IconChevronLeft />
|
|
||||||
// </ActionIcon>
|
|
||||||
// </Tooltip>
|
|
||||||
// </Group>
|
|
||||||
// </AppShell.Section>
|
|
||||||
// </AppShellNavbar>
|
|
||||||
|
|
||||||
// <AppShellMain
|
|
||||||
// style={{
|
|
||||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
|
||||||
// minHeight: "100vh",
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {children}
|
|
||||||
// </AppShellMain>
|
|
||||||
// </AppShell>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// app/admin/layout.tsx
|
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
@@ -426,6 +30,7 @@ import _ from "lodash";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
// import { useSnapshot } from "valtio";
|
||||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
@@ -435,53 +40,42 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
|
|
||||||
|
// const { user } = useSnapshot(authStore);
|
||||||
|
|
||||||
|
// console.log("Current user in store:", user);
|
||||||
|
|
||||||
|
// ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/me');
|
||||||
credentials: 'include' // ✅ ADD credentials
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
// ✅ Check if user is NOT active → redirect to waiting room
|
// Check if user is active
|
||||||
if (!data.user.isActive) {
|
if (!data.user.isActive) {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/waiting-room');
|
router.replace('/waiting-room');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fetch menuIds
|
// ✅ PENTING: Selalu fetch menuIds terbaru setiap login
|
||||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||||
credentials: 'include' // ✅ ADD credentials
|
|
||||||
});
|
|
||||||
const menuData = await menuRes.json();
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||||
? [...menuData.menuIds]
|
? [...menuData.menuIds]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ✅ Set user dengan menuIds yang fresh
|
// ✅ Set user dengan menuIds yang fresh dari database
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
roleId: Number(data.user.roleId),
|
roleId: Number(data.user.roleId),
|
||||||
menuIds,
|
menuIds, // menuIds terbaru
|
||||||
isActive: data.user.isActive
|
isActive: data.user.isActive
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
|
|
||||||
if (currentPath === '/admin') {
|
|
||||||
const expectedPath = getRedirectPath(Number(data.user.roleId));
|
|
||||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
|
||||||
router.replace(expectedPath);
|
|
||||||
}
|
|
||||||
// ✅ Jangan redirect jika user sudah di path yang valid
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
@@ -496,22 +90,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [router]); // ✅ Only depend on router
|
}, [router]); // ✅ Hapus dependency pada authStore.user
|
||||||
|
|
||||||
const getRedirectPath = (roleId: number): string => {
|
|
||||||
switch (roleId) {
|
|
||||||
case 0: // DEVELOPER
|
|
||||||
case 1: // SUPERADMIN
|
|
||||||
case 2: // ADMIN_DESA
|
|
||||||
return '/admin/landing-page/profil/program-inovasi';
|
|
||||||
case 3: // ADMIN_KESEHATAN
|
|
||||||
return '/admin/kesehatan/posyandu';
|
|
||||||
case 4: // ADMIN_PENDIDIKAN
|
|
||||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
|
||||||
default:
|
|
||||||
return '/admin';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -525,6 +104,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Ambil menu berdasarkan roleId dan menuIds
|
||||||
const currentNav = authStore.user
|
const currentNav = authStore.user
|
||||||
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||||
: [];
|
: [];
|
||||||
@@ -532,26 +112,30 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
|
|
||||||
const response = await fetch('/api/auth/logout', {
|
// ✅ Panggil API logout untuk clear session di server
|
||||||
method: 'POST',
|
const response = await fetch('/api/logout', { method: 'POST' });
|
||||||
credentials: 'include' // ✅ ADD credentials
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
// Clear user data dari store
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
|
|
||||||
|
// Clear localStorage
|
||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
localStorage.removeItem('auth_kodeId');
|
localStorage.removeItem('auth_kodeId');
|
||||||
localStorage.removeItem('auth_username');
|
|
||||||
|
// Force reload untuk reset semua state
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} else {
|
} else {
|
||||||
console.error('Logout failed:', result.message);
|
console.error('Logout failed:', result.message);
|
||||||
|
// Tetap redirect meskipun gagal
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during logout:', error);
|
console.error('Error during logout:', error);
|
||||||
|
// Tetap clear store dan redirect jika error
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -573,7 +157,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||||
@@ -592,9 +175,16 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
h={{ base: 32, sm: 40 }}
|
h={{ base: 32, sm: 40 }}
|
||||||
radius="md"
|
radius="md"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ minWidth: '32px', height: 'auto' }}
|
style={{
|
||||||
|
minWidth: '32px',
|
||||||
|
height: 'auto',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text fw={700} c={colors["blue-button"]} fz={{ base: 'md', sm: 'xl' }}>
|
<Text
|
||||||
|
fw={700}
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fz={{ base: 'md', sm: 'xl' }}
|
||||||
|
>
|
||||||
Admin Darmasaba
|
Admin Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -602,22 +192,63 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{!desktopOpened && (
|
{!desktopOpened && (
|
||||||
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
onClick={toggleDesktop}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
>
|
||||||
<IconChevronRight />
|
<IconChevronRight />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="md" color={colors["blue-button"]} mr="xs" />
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggle}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="md"
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
mr="xs"
|
||||||
|
/>
|
||||||
|
|
||||||
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||||
<ActionIcon onClick={() => router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}>
|
<ActionIcon
|
||||||
<Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={20} h={20} radius="md" loading="lazy" style={{ minWidth: '20px', height: 'auto' }} />
|
onClick={() => {
|
||||||
|
router.push("/darmasaba");
|
||||||
|
}}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/images/darmasaba-icon.png"
|
||||||
|
alt="Logo Darmasaba"
|
||||||
|
w={20}
|
||||||
|
h={20}
|
||||||
|
radius="md"
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
minWidth: '20px',
|
||||||
|
height: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon onClick={handleLogout} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }} loading={isLoggingOut} disabled={isLoggingOut}>
|
<ActionIcon
|
||||||
|
onClick={handleLogout}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
|
loading={isLoggingOut}
|
||||||
|
disabled={isLoggingOut}
|
||||||
|
>
|
||||||
<IconLogout2 size={22} />
|
<IconLogout2 size={22} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -625,17 +256,75 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
<AppShellNavbar
|
||||||
{/* ... Navbar content sama seperti sebelumnya ... */}
|
component={ScrollArea}
|
||||||
|
style={{
|
||||||
|
background: "#ffffff",
|
||||||
|
borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||||
|
}}
|
||||||
|
p={{ base: 'xs', sm: 'sm' }}
|
||||||
|
>
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{currentNav.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
<NavLink
|
||||||
|
key={k}
|
||||||
|
defaultOpened={isParentActive}
|
||||||
|
c={isParentActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={
|
||||||
|
<Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||||
|
{v.name}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderRadius: rem(10),
|
||||||
|
marginBottom: rem(4),
|
||||||
|
transition: "background 150ms ease",
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
active={isParentActive}
|
||||||
|
>
|
||||||
{v.children.map((child, key) => {
|
{v.children.map((child, key) => {
|
||||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
const isChildActive = segments.includes(
|
||||||
|
_.lowerCase(child.name)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
<NavLink
|
||||||
|
key={key}
|
||||||
|
href={child.path}
|
||||||
|
c={isChildActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={
|
||||||
|
<Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||||
|
{child.name}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: rem(8),
|
||||||
|
marginBottom: rem(2),
|
||||||
|
transition: 'background 150ms ease',
|
||||||
|
padding: '6px 12px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||||
|
},
|
||||||
|
...(isChildActive && {
|
||||||
|
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
active={isChildActive}
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -645,8 +334,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
<AppShell.Section py="md">
|
<AppShell.Section py="md">
|
||||||
<Group justify="end" pr="sm">
|
<Group justify="end" pr="sm">
|
||||||
<Tooltip label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"} position="top" withArrow>
|
<Tooltip
|
||||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||||
|
position="top"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
onClick={toggleDesktop}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
>
|
||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -654,7 +353,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShell.Section>
|
</AppShell.Section>
|
||||||
</AppShellNavbar>
|
</AppShellNavbar>
|
||||||
|
|
||||||
<AppShellMain style={{ background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)", minHeight: "100vh" }}>
|
<AppShellMain
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||||
|
minHeight: "100vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FasilitasKesehatanInput = {
|
||||||
|
name: string;
|
||||||
|
informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string };
|
||||||
|
layananUnggulan: { content: string };
|
||||||
|
dokterdanTenagaMedis: { name: string; specialist: string; jadwal: string };
|
||||||
|
fasilitasPendukung: { content: string };
|
||||||
|
prosedurPendaftaran: { content: string };
|
||||||
|
tarifDanLayanan: { layanan: string; tarif: string };
|
||||||
|
};
|
||||||
|
|
||||||
const fasilitasKesehatanCreate = async (context: Context) => {
|
const fasilitasKesehatanCreate = async (context: Context) => {
|
||||||
const body = (await context.body) as {
|
const body = await context.body as FasilitasKesehatanInput;
|
||||||
name: string;
|
|
||||||
informasiUmum: { fasilitas: string; alamat: string; jamOperasional: string };
|
|
||||||
layananUnggulan: { content: string };
|
|
||||||
dokterdanTenagaMedis: string[]; // ← ARRAY OF ID
|
|
||||||
fasilitasPendukung: { content: string };
|
|
||||||
prosedurPendaftaran: { content: string };
|
|
||||||
tarifDanLayanan: string[]; // ← ARRAY OF ID
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@@ -22,30 +24,25 @@ const fasilitasKesehatanCreate = async (context: Context) => {
|
|||||||
tarifDanLayanan,
|
tarifDanLayanan,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
// CREATE SINGLE DATA
|
// Buat masing-masing relasi terlebih dahulu
|
||||||
const [createdInformasi, createdUnggulan, createdPendukung, createdProsedur] =
|
const [createdInformasiUmum, createdLayananUnggulan, createdDokter, createdPendukung, createdProsedur, createdTarif] = await Promise.all([
|
||||||
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: createdInformasi.id,
|
informasiUmumId: createdInformasiUmum.id,
|
||||||
layananUnggulanId: createdUnggulan.id,
|
layananUnggulanId: createdLayananUnggulan.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,
|
||||||
|
|||||||
@@ -2,14 +2,42 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
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;
|
||||||
|
|
||||||
const data = await prisma.fasilitasKesehatan.findUnique({ where: { id } });
|
if (!id) {
|
||||||
if (!data) return { status: 404, message: "Data tidak ditemukan" };
|
return {
|
||||||
|
status: 400,
|
||||||
|
message: "ID tidak ditemukan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.fasilitasKesehatan.delete({ where: { id } });
|
const fasilitasKesehatan = await prisma.fasilitasKesehatan.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
informasiumum: true,
|
||||||
|
layananunggulan: true,
|
||||||
|
dokterdantenagamedis: true,
|
||||||
|
fasilitaspendukung: true,
|
||||||
|
prosedurpendaftaran: true,
|
||||||
|
tarifdanlayanan: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return { success: true, message: "Berhasil dihapus" };
|
if (!fasilitasKesehatan) {
|
||||||
};
|
return {
|
||||||
|
status: 404,
|
||||||
|
message: "Fasilitas kesehatan tidak ditemukan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default fasilitasKesehatanDelete;
|
await prisma.fasilitasKesehatan.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
success: true,
|
||||||
|
message: "Fasilitas kesehatan berhasil dihapus",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default fasilitasKesehatanDelete
|
||||||
@@ -5,11 +5,6 @@ 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) {
|
||||||
@@ -20,21 +15,11 @@ 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,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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' } },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ 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, {
|
||||||
@@ -31,11 +26,6 @@ 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)
|
||||||
|
|||||||
@@ -5,11 +5,6 @@ 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) {
|
||||||
@@ -23,12 +18,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import fasilitasKesehatanCreate from "./create";
|
import fasilitasKesehatanCreate from "./create";
|
||||||
import fasilitasKesehatanFindMany from "./findMany";
|
import findManyFasilitasKesehatan 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,61 +9,42 @@ 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({
|
||||||
dokterdanTenagaMedis: t.Array(t.String()), // FIX karena create pakai array of string
|
name: t.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({
|
||||||
tarifDanLayanan: t.Array(t.String()), // FIX karena create pakai array of string
|
layanan: t.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) => {
|
||||||
@@ -73,30 +54,29 @@ 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({
|
||||||
// FIX → harus array of string (ID dokter)
|
name: t.String(),
|
||||||
dokterdanTenagaMedis: t.Array(t.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({
|
||||||
// FIX → harus array of string (ID tarif)
|
layanan: t.String(),
|
||||||
tarifDanLayanan: t.Array(t.String()),
|
tarif: t.String(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,26 +5,32 @@ 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: string[]; // ← ID saja
|
dokterdanTenagaMedis: { name: string; specialist: string; jadwal: string };
|
||||||
fasilitasPendukung: { content: string };
|
fasilitasPendukung: { content: string };
|
||||||
prosedurPendaftaran: { content: string };
|
prosedurPendaftaran: { content: string };
|
||||||
tarifDanLayanan: string[]; // ← ID saja
|
tarifDanLayanan: { layanan: string; tarif: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
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 { success: false, message: "Data tidak ditemukan" };
|
return new Response(
|
||||||
|
JSON.stringify({ success: false, message: "Data not found" }),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -37,46 +43,38 @@ const fasilitasKesehatanUpdate = async (context: Context) => {
|
|||||||
tarifDanLayanan,
|
tarifDanLayanan,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
// update relasi 1-1
|
// Update data masing-masing relasi
|
||||||
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 m2m
|
// Update main record
|
||||||
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,
|
||||||
@@ -89,7 +87,7 @@ const fasilitasKesehatanUpdate = async (context: Context) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Fasilitas diupdate",
|
message: "Fasilitas berhasil diupdate",
|
||||||
data: updated,
|
data: updated,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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({
|
||||||
@@ -47,6 +46,5 @@ 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;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ type FormCreate = {
|
|||||||
name: string;
|
name: string;
|
||||||
imageId: string;
|
imageId: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
icon: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function mediaSosialCreate(context: Context) {
|
export default async function mediaSosialCreate(context: Context) {
|
||||||
@@ -15,9 +14,8 @@ export default async function mediaSosialCreate(context: Context) {
|
|||||||
const result = await prisma.mediaSosial.create({
|
const result = await prisma.mediaSosial.create({
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
imageId: body.imageId || null,
|
imageId: body.imageId,
|
||||||
iconUrl: body.iconUrl,
|
iconUrl: body.iconUrl,
|
||||||
icon: body.icon || null,
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
@@ -31,6 +29,8 @@ export default async function mediaSosialCreate(context: Context) {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating media sosial:", error);
|
console.error("Error creating media sosial:", error);
|
||||||
throw new Error("Gagal membuat media sosial: " + (error as Error).message);
|
throw new Error(
|
||||||
|
"Gagal membuat media sosial: " + (error as Error).message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ const MediaSosial = new Elysia({
|
|||||||
.post("/create", MediaSosialCreate, {
|
.post("/create", MediaSosialCreate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
imageId: t.Union([t.String(), t.Null()]),
|
imageId: t.String(),
|
||||||
iconUrl: t.Union([t.String(), t.Null()]),
|
iconUrl: t.String(),
|
||||||
icon: t.Union([t.String(), t.Null()]),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,9 +29,8 @@ const MediaSosial = new Elysia({
|
|||||||
.put("/:id", MediaSosialUpdate, {
|
.put("/:id", MediaSosialUpdate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
imageId: t.Optional(t.Union([t.String(), t.Null()])),
|
imageId: t.Optional(t.String()),
|
||||||
iconUrl: t.Optional(t.Union([t.String(), t.Null()])),
|
iconUrl: t.Optional(t.String()),
|
||||||
icon: t.Optional(t.Union([t.String(), t.Null()])),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
// ✅ Delete
|
// ✅ Delete
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type FormUpdateMediaSosial = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
imageId?: string;
|
imageId?: string;
|
||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
icon?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function mediaSosialUpdate(context: Context) {
|
export default async function mediaSosialUpdate(context: Context) {
|
||||||
@@ -21,29 +20,13 @@ export default async function mediaSosialUpdate(context: Context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚨 Tambahkan validasi di sini
|
|
||||||
if (!body.name || body.name.trim().length < 3) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Nama media sosial minimal 3 karakter",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!body.iconUrl || body.iconUrl.trim().length < 3) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Icon URL minimal 3 karakter",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await prisma.mediaSosial.update({
|
const updated = await prisma.mediaSosial.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
imageId: body.imageId || null, // pastikan null jika kosong
|
imageId: body.imageId,
|
||||||
iconUrl: body.iconUrl,
|
iconUrl: body.iconUrl,
|
||||||
icon: body.icon || null, // pastikan null jika kosong
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
|
|||||||
@@ -557,37 +557,25 @@ 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: {
|
||||||
skip,
|
OR: [
|
||||||
take: limitNum,
|
{ layanan: { contains: query, mode: "insensitive" } },
|
||||||
});
|
{ tarif: { contains: query, mode: "insensitive" } },
|
||||||
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,
|
||||||
@@ -1579,8 +1567,6 @@ 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" } },
|
||||||
@@ -1908,27 +1894,25 @@ 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: {
|
||||||
take: limitNum,
|
OR: [
|
||||||
}),
|
{ layanan: { contains: query, mode: "insensitive" } },
|
||||||
prisma.dokterdanTenagaMedis.findMany({
|
{ tarif: { contains: query, mode: "insensitive" } },
|
||||||
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,
|
||||||
}),
|
}),
|
||||||
@@ -2332,7 +2316,7 @@ export default async function searchFindMany(context: Context) {
|
|||||||
{ judul: { contains: query, mode: "insensitive" } },
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
|
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
|
||||||
{ deskripsiLengkap: { contains: query, mode: "insensitive" } },
|
{ deskripsiLengkap: { contains: query, mode: "insensitive" } },
|
||||||
{ lokasi: { contains: query, mode: "insensitive" } },
|
{ lokasi: { contains: query, mode: "insensitive" } },
|
||||||
{
|
{
|
||||||
kategoriKegiatan: {
|
kategoriKegiatan: {
|
||||||
nama: { contains: query, mode: "insensitive" },
|
nama: { contains: query, mode: "insensitive" },
|
||||||
@@ -2575,8 +2559,6 @@ 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 })),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default async function userUpdate(context: Context) {
|
|||||||
|
|
||||||
const currentUser = await prisma.user.findUnique({
|
const currentUser = await prisma.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
select: { roleId: true, isActive: true },
|
select: { roleId: true, isActive: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
@@ -31,15 +31,7 @@ export default async function userUpdate(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRoleChanged = roleId && currentUser.roleId !== roleId;
|
const isRoleChanged = roleId && currentUser.roleId !== roleId;
|
||||||
const isActiveChanged =
|
const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive;
|
||||||
isActive !== undefined && currentUser.isActive !== isActive;
|
|
||||||
|
|
||||||
// ✅ Jika role berubah, hapus semua akses menu yang ada
|
|
||||||
if (isRoleChanged) {
|
|
||||||
await prisma.userMenuAccess.deleteMany({
|
|
||||||
where: { userId: id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
@@ -56,11 +48,10 @@ export default async function userUpdate(context: Context) {
|
|||||||
nomor: true,
|
nomor: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
roleId: true,
|
roleId: true,
|
||||||
role: { select: { name: true } },
|
role: { select: { name: true } }
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// ✅ HAPUS SEMUA SESI USER DI DATABASE
|
// ✅ HAPUS SEMUA SESI USER DI DATABASE
|
||||||
if (isRoleChanged) {
|
if (isRoleChanged) {
|
||||||
await prisma.userSession.deleteMany({ where: { userId: id } });
|
await prisma.userSession.deleteMany({ where: { userId: id } });
|
||||||
@@ -71,13 +62,11 @@ export default async function userUpdate(context: Context) {
|
|||||||
roleChanged: isRoleChanged,
|
roleChanged: isRoleChanged,
|
||||||
isActiveChanged,
|
isActiveChanged,
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
message: isRoleChanged
|
message: isRoleChanged
|
||||||
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
|
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
|
||||||
: isActiveChanged
|
: isActiveChanged
|
||||||
? `${updatedUser.username} ${
|
? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.`
|
||||||
isActive ? "diaktifkan" : "dinonaktifkan"
|
: "User berhasil diupdate"
|
||||||
}.`
|
|
||||||
: "User berhasil diupdate",
|
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("❌ Error update user:", e);
|
console.error("❌ Error update user:", e);
|
||||||
@@ -86,4 +75,4 @@ export default async function userUpdate(context: Context) {
|
|||||||
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
// app/api/auth/_lib/api_fetch_auth.ts
|
// app/api/_lib/api_fetch_auth.ts
|
||||||
|
|
||||||
// app/api/auth/_lib/api_fetch_auth.ts
|
// app/api/_lib/api_fetch_auth.ts
|
||||||
|
|
||||||
export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
||||||
if (!nomor || nomor.replace(/\D/g, '').length < 10) {
|
if (!nomor || nomor.replace(/\D/g, '').length < 10) {
|
||||||
@@ -10,7 +10,7 @@ export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
|||||||
|
|
||||||
const cleanPhone = nomor.replace(/\D/g, '');
|
const cleanPhone = nomor.replace(/\D/g, '');
|
||||||
|
|
||||||
const response = await fetch("/api/auth/login", {
|
const response = await fetch("/api/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ nomor: cleanPhone }),
|
body: JSON.stringify({ nomor: cleanPhone }),
|
||||||
@@ -22,7 +22,7 @@ export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
|||||||
try {
|
try {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Non-JSON response from /api/auth/login:", await response.text());
|
console.error("Non-JSON response from /api/login:", await response.text());
|
||||||
throw new Error('Respons server tidak valid');
|
throw new Error('Respons server tidak valid');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export const apiFetchRegister = async ({
|
|||||||
const cleanPhone = nomor.replace(/\D/g, '');
|
const cleanPhone = nomor.replace(/\D/g, '');
|
||||||
if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
|
if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
|
||||||
|
|
||||||
const response = await fetch("/api/auth/register", {
|
const response = await fetch("/api/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
|
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
|
||||||
@@ -73,7 +73,7 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
|
|||||||
throw new Error('Kode ID tidak valid');
|
throw new Error('Kode ID tidak valid');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch("/api/auth/otp-data", {
|
const response = await fetch("/api/otp-data", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ kodeId }),
|
body: JSON.stringify({ kodeId }),
|
||||||
@@ -90,7 +90,7 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
|
|||||||
|
|
||||||
// Ganti endpoint ke verify-otp-login
|
// Ganti endpoint ke verify-otp-login
|
||||||
export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => {
|
export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => {
|
||||||
const response = await fetch('/api/auth/verify-otp-login', {
|
const response = await fetch('/api/verify-otp-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, otp, kodeId }),
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
@@ -30,12 +30,7 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify OTP
|
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||||
const otpRecord = await prisma.kodeOtp.findUnique({
|
|
||||||
where: { id: kodeId },
|
|
||||||
select: { nomor: true, isActive: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "OTP tidak valid" },
|
{ success: false, message: "OTP tidak valid" },
|
||||||
@@ -43,7 +38,6 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check duplicate username
|
|
||||||
if (await prisma.user.findFirst({ where: { username } })) {
|
if (await prisma.user.findFirst({ where: { username } })) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Username sudah digunakan" },
|
{ success: false, message: "Username sudah digunakan" },
|
||||||
@@ -51,20 +45,12 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check duplicate nomor
|
|
||||||
if (await prisma.user.findUnique({ where: { nomor: cleanNomor } })) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Nomor sudah terdaftar" },
|
|
||||||
{ status: 409 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔥 Tentukan roleId sebagai STRING
|
// 🔥 Tentukan roleId sebagai STRING
|
||||||
const targetRoleId = "2"; // ✅ Default ADMIN_DESA (roleId "2")
|
const targetRoleId = "1"; // ✅ string, bukan number
|
||||||
|
|
||||||
// Validasi role exists
|
// Validasi role (gunakan string)
|
||||||
const roleExists = await prisma.role.findUnique({
|
const roleExists = await prisma.role.findUnique({
|
||||||
where: { id: targetRoleId },
|
where: { id: targetRoleId }, // ✅ id bertipe string
|
||||||
select: { id: true }
|
select: { id: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,17 +61,17 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Create user (inactive, waiting approval)
|
// Buat user dengan roleId string
|
||||||
const newUser = await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
username,
|
username,
|
||||||
nomor: cleanNomor,
|
nomor,
|
||||||
roleId: targetRoleId,
|
roleId: targetRoleId, // ✅ string
|
||||||
isActive: false, // Waiting for admin approval
|
isActive: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Berikan akses menu default based on role
|
// Berikan akses menu
|
||||||
const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || [];
|
const menuIds = DEFAULT_MENUS_BY_ROLE[targetRoleId] || [];
|
||||||
if (menuIds.length > 0) {
|
if (menuIds.length > 0) {
|
||||||
await prisma.userMenuAccess.createMany({
|
await prisma.userMenuAccess.createMany({
|
||||||
@@ -93,17 +79,14 @@ export async function POST(req: Request) {
|
|||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
menuId,
|
menuId,
|
||||||
})),
|
})),
|
||||||
skipDuplicates: true, // ✅ Avoid duplicate errors
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Mark OTP as used
|
|
||||||
await prisma.kodeOtp.update({
|
await prisma.kodeOtp.update({
|
||||||
where: { id: kodeId },
|
where: { id: kodeId },
|
||||||
data: { isActive: false },
|
data: { isActive: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Create session token
|
|
||||||
const token = await sessionCreate({
|
const token = await sessionCreate({
|
||||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
@@ -112,35 +95,25 @@ export async function POST(req: Request) {
|
|||||||
id: newUser.id,
|
id: newUser.id,
|
||||||
nomor: newUser.nomor,
|
nomor: newUser.nomor,
|
||||||
username: newUser.username,
|
username: newUser.username,
|
||||||
roleId: newUser.roleId,
|
roleId: newUser.roleId, // string
|
||||||
isActive: false, // User belum aktif
|
isActive: false,
|
||||||
},
|
},
|
||||||
invalidatePrevious: false,
|
invalidatePrevious: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ PENTING: Return JSON response (bukan redirect)
|
const response = NextResponse.redirect(new URL('/waiting-room', req.url));
|
||||||
const response = NextResponse.json({
|
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||||
success: true,
|
|
||||||
message: "Registrasi berhasil. Menunggu persetujuan admin.",
|
|
||||||
userId: newUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Set session cookie
|
|
||||||
const cookieName = process.env.BASE_SESSION_KEY || 'session';
|
|
||||||
response.cookies.set(cookieName, token, {
|
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
sameSite: 'lax',
|
|
||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
maxAge: 30 * 24 * 60 * 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Finalize Registration Error:", error);
|
console.error("❌ Finalize Registration Error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Registrasi gagal. Silakan coba lagi." },
|
{ success: false, message: "Registrasi gagal" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -12,7 +12,6 @@ export async function GET() {
|
|||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const [dbUser, menuAccess] = await Promise.all([
|
const [dbUser, menuAccess] = await Promise.all([
|
||||||
prisma.user.findUnique({
|
prisma.user.findUnique({
|
||||||
@@ -51,7 +50,7 @@ export async function GET() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error in /api/auth/me:", error);
|
console.error("❌ Error in /api/me:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Internal server error", user: null },
|
{ success: false, message: "Internal server error", user: null },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -42,7 +42,9 @@ 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',
|
||||||
@@ -209,7 +211,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 aria-label="Tabel Dokter">
|
<Table highlightOnHover withTableBorder withColumnBorders stickyHeader stickyHeaderOffset={0} aria-label="Tabel Dokter">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh>Nama</TableTh>
|
||||||
@@ -218,19 +220,12 @@ function Page() {
|
|||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{Array.isArray(data?.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? (
|
{tenaga ? (
|
||||||
data.dokterdantenagamedis.map((dokter: any) => (
|
<TableTr>
|
||||||
<TableTr key={dokter.id}>
|
<TableTd><Group gap="xs"><IconUser size={16} /><Text>{tenaga?.name || '-'}</Text></Group></TableTd>
|
||||||
<TableTd>
|
<TableTd>{tenaga?.specialist || '-'}</TableTd>
|
||||||
<Group gap="xs">
|
<TableTd>{tenaga?.jadwal || '-'}</TableTd>
|
||||||
<IconUser size={16} />
|
</TableTr>
|
||||||
<Text>{dokter.name || '-'}</Text>
|
|
||||||
</Group>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>{dokter.specialist || '-'}</TableTd>
|
|
||||||
<TableTd>{dokter.jadwal || '-'}</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={3}>
|
<TableTd colSpan={3}>
|
||||||
@@ -246,74 +241,72 @@ function Page() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card radius="xl" p="lg" withBorder>
|
<Card radius="xl" p="lg" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Fasilitas Pendukung</Title>
|
<Title order={3}>Fasilitas Pendukung</Title>
|
||||||
<Divider />
|
<Divider />
|
||||||
{fasilitasPendukungHtml ? (
|
{fasilitasPendukungHtml ? (
|
||||||
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: fasilitasPendukungHtml }} />
|
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: fasilitasPendukungHtml }} />
|
||||||
) : (
|
) : (
|
||||||
<Paper withBorder radius="md" p="md">
|
<Paper withBorder radius="md" p="md">
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<IconMoodEmpty />
|
<IconMoodEmpty />
|
||||||
<Text>Belum ada informasi fasilitas pendukung.</Text>
|
<Text>Belum ada informasi fasilitas pendukung.</Text>
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card radius="xl" p="lg" withBorder>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Title order={3}>Layanan & Tarif</Title>
|
|
||||||
<Divider />
|
|
||||||
<Table highlightOnHover withTableBorder withColumnBorders aria-label="Tabel Layanan dan Tarif">
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Layanan</TableTh>
|
|
||||||
<TableTh>Tarif</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{Array.isArray(data?.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
|
|
||||||
data.tarifdanlayanan.map((item: any) => (
|
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>{item.layanan || '-'}</TableTd>
|
|
||||||
<TableTd>{formatRupiah(item.tarif)}</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableTr>
|
|
||||||
<TableTd colSpan={2}>
|
|
||||||
<Group justify="center" gap="xs" c="dimmed">
|
|
||||||
<IconSearch size={18} />
|
|
||||||
<Text>Tidak ada data tarif.</Text>
|
|
||||||
</Group>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
{gratisBpjs && (
|
|
||||||
<Group gap="xs">
|
|
||||||
<ThemeIcon variant="light" radius="xl"><IconCheck size={18} /></ThemeIcon>
|
|
||||||
<Text fw={600}>Gratis dengan BPJS Kesehatan</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
</Paper>
|
||||||
</Stack>
|
)}
|
||||||
</Card>
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card radius="xl" p="lg" withBorder>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Title order={3}>Layanan & Tarif</Title>
|
||||||
|
<Divider />
|
||||||
|
<Table highlightOnHover withTableBorder withColumnBorders aria-label="Tabel Layanan dan Tarif">
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Layanan</TableTh>
|
||||||
|
<TableTh>Tarif</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{tarif ? (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd>{tarif?.layanan || '-'}</TableTd>
|
||||||
|
<TableTd>{formatRupiah(tarif?.tarif)}</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={2}>
|
||||||
|
<Group justify="center" gap="xs" c="dimmed">
|
||||||
|
<IconSearch size={18} />
|
||||||
|
<Text>Tidak ada data tarif.</Text>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
{gratisBpjs && (
|
||||||
|
<Group gap="xs">
|
||||||
|
<ThemeIcon variant="light" radius="xl"><IconCheck size={18} /></ThemeIcon>
|
||||||
|
<Text fw={600}>Gratis dengan BPJS Kesehatan</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box px={{ base: 'md', md: 100 }} pb="xl">
|
<Box px={{ base: 'md', md: 100 }} pb="xl">
|
||||||
<Paper radius="xl" p="lg" withBorder>
|
<Paper radius="xl" p="lg" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Prosedur Pendaftaran</Title>
|
<Title order={3}>Prosedur Pendaftaran</Title>
|
||||||
<Divider />
|
<Divider />
|
||||||
{prosedur ? (
|
{prosedur ? (
|
||||||
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: prosedur }} />
|
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: prosedur }} />
|
||||||
) : (
|
) : (
|
||||||
<Text fz="md" c="dimmed">Belum ada prosedur pendaftaran</Text>
|
<Text fz="md" c="dimmed">Belum ada prosedur pendaftaran</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik/${item.id}`)}
|
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// src/app/admin/(dashboard)/landing-page/APBDes/APBDesProgress.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Paper, Progress, Stack, Text, Title } from '@mantine/core';
|
import { Box, Paper, Progress, Stack, Text, Title } from '@mantine/core';
|
||||||
import { APBDesData } from './types';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function formatRupiah(value: number) {
|
function formatRupiah(value: number) {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
@@ -12,33 +17,31 @@ function formatRupiah(value: number) {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APBDesProgressProps {
|
function APBDesProgress() {
|
||||||
apbdesData: APBDesData;
|
const state = useProxy(apbdes);
|
||||||
}
|
const data = state.findMany.data || [];
|
||||||
|
|
||||||
function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
// Ambil APBDes pertama (misalnya, jika hanya satu tahun ditampilkan)
|
||||||
// Return null if apbdesData is not available yet
|
const apbdesItem = data[0]; // 👈 sesuaikan logika jika ada banyak APBDes
|
||||||
if (!apbdesData) {
|
|
||||||
return null;
|
if (!apbdesItem) {
|
||||||
|
return (
|
||||||
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
<Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = apbdesData.items || [];
|
const items = apbdesItem.items || [];
|
||||||
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
// Kelompokkan berdasarkan tipe
|
// Kelompokkan berdasarkan tipe
|
||||||
const pendapatanItems = sortedItems.filter(item => item.tipe === 'pendapatan');
|
const pendapatanItems = sortedItems.filter(item => item.tipe === 'pendapatan');
|
||||||
const belanjaItems = sortedItems.filter(item => item.tipe === 'belanja');
|
const belanjaItems = sortedItems.filter(item => item.tipe === 'belanja');
|
||||||
const pembiayaanItems = sortedItems.filter(item => item.tipe === 'pembiayaan');
|
const pembiayaanItems = sortedItems.filter(item => item.tipe === 'pembiayaan'); // jika ada
|
||||||
|
|
||||||
// Items without a type (should be filtered out from calculations)
|
|
||||||
const untypedItems = sortedItems.filter(item => !item.tipe);
|
|
||||||
|
|
||||||
if (untypedItems.length > 0) {
|
|
||||||
console.warn(`Found ${untypedItems.length} items without a type. These will be excluded from calculations.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hitung total per kategori
|
// Hitung total per kategori
|
||||||
const calcTotal = (items: { anggaran: number; realisasi: number }[]) => {
|
const calcTotal = (items: any[]) => {
|
||||||
const anggaran = items.reduce((sum, item) => sum + item.anggaran, 0);
|
const anggaran = items.reduce((sum, item) => sum + item.anggaran, 0);
|
||||||
const realisasi = items.reduce((sum, item) => sum + item.realisasi, 0);
|
const realisasi = items.reduce((sum, item) => sum + item.realisasi, 0);
|
||||||
const persen = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
const persen = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
||||||
@@ -47,10 +50,10 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
|
|
||||||
const pendapatan = calcTotal(pendapatanItems);
|
const pendapatan = calcTotal(pendapatanItems);
|
||||||
const belanja = calcTotal(belanjaItems);
|
const belanja = calcTotal(belanjaItems);
|
||||||
const pembiayaan = calcTotal(pembiayaanItems);
|
const pembiayaan = calcTotal(pembiayaanItems); // bisa kosong
|
||||||
|
|
||||||
// Render satu progress bar
|
// Render satu progress bar
|
||||||
const renderProgress = (label: string, dataset: { realisasi: number; anggaran: number; persen: number }) => {
|
const renderProgress = (label: string, dataset: any) => {
|
||||||
const isPembiayaan = label.includes('Pembiayaan');
|
const isPembiayaan = label.includes('Pembiayaan');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,8 +71,8 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
root: { backgroundColor: '#d7e3f1' },
|
root: { backgroundColor: '#d7e3f1' },
|
||||||
section: {
|
section: {
|
||||||
backgroundColor: isPembiayaan
|
backgroundColor: isPembiayaan
|
||||||
? 'green'
|
? 'green' // warna hijau untuk pembiayaan
|
||||||
: colors['blue-button'],
|
: colors['blue-button'], // biru untuk pendapatan/belanja
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
'&::after': {
|
'&::after': {
|
||||||
content: `'${dataset.persen.toFixed(2)}%'`,
|
content: `'${dataset.persen.toFixed(2)}%'`,
|
||||||
@@ -99,7 +102,7 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={4} c={colors['blue-button']} ta="center">
|
<Title order={4} c={colors['blue-button']} ta="center">
|
||||||
Grafik Pelaksanaan APBDes Tahun {apbdesData.tahun}
|
Grafik Pelaksanaan APBDes Tahun {apbdesItem.tahun}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text ta="center" fw="bold" fz="sm" c="dimmed">
|
<Text ta="center" fw="bold" fz="sm" c="dimmed">
|
||||||
@@ -109,9 +112,97 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
{renderProgress('Pendapatan Desa', pendapatan)}
|
{renderProgress('Pendapatan Desa', pendapatan)}
|
||||||
{renderProgress('Belanja Desa', belanja)}
|
{renderProgress('Belanja Desa', belanja)}
|
||||||
{renderProgress('Pembiayaan Desa', pembiayaan)}
|
{renderProgress('Pembiayaan Desa', pembiayaan)}
|
||||||
|
{pembiayaanItems.length > 0 && renderProgress('Pembiayaan Desa', pembiayaan)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APBDesProgress;
|
export default APBDesProgress;
|
||||||
|
|
||||||
|
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { Box, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
|
// import { BarChart } from '@mantine/charts';
|
||||||
|
// import { useProxy } from 'valtio/utils';
|
||||||
|
// import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
// import colors from '@/con/colors';
|
||||||
|
|
||||||
|
// function APBDesProgress() {
|
||||||
|
// const state = useProxy(apbdes);
|
||||||
|
// const data = state.findMany.data || [];
|
||||||
|
|
||||||
|
// const apbdesItem = data[0];
|
||||||
|
// if (!apbdesItem) {
|
||||||
|
// return (
|
||||||
|
// <Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
// <Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const items = apbdesItem.items || [];
|
||||||
|
// const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
|
// const pendapatanItems = sortedItems.filter(i => i.tipe === 'pendapatan');
|
||||||
|
// const belanjaItems = sortedItems.filter(i => i.tipe === 'belanja');
|
||||||
|
// const pembiayaanItems = sortedItems.filter(i => i.tipe === 'pembiayaan');
|
||||||
|
|
||||||
|
// const total = (rows: any[]) => {
|
||||||
|
// const anggaran = rows.reduce((s, i) => s + i.anggaran, 0);
|
||||||
|
// const realisasi = rows.reduce((s, i) => s + i.realisasi, 0);
|
||||||
|
// return anggaran === 0 ? 0 : (realisasi / anggaran) * 100;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const chartData = [
|
||||||
|
// { name: 'Pendapatan', persen: total(pendapatanItems) },
|
||||||
|
// { name: 'Belanja', persen: total(belanjaItems) },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// if (pembiayaanItems.length > 0) {
|
||||||
|
// chartData.push({ name: 'Pembiayaan', persen: total(pembiayaanItems) });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Paper
|
||||||
|
// mx={{ base: 'md', md: 100 }}
|
||||||
|
// p="xl"
|
||||||
|
// radius="md"
|
||||||
|
// shadow="sm"
|
||||||
|
// withBorder
|
||||||
|
// bg={colors['white-1']}
|
||||||
|
// >
|
||||||
|
// <Stack gap="lg">
|
||||||
|
// <Title order={4} c={colors['blue-button']} ta="center">
|
||||||
|
// Grafik Pelaksanaan APBDes Tahun {apbdesItem.tahun}
|
||||||
|
// </Title>
|
||||||
|
|
||||||
|
// <Text ta="center" fw="bold" fz="sm" c="dimmed">
|
||||||
|
// Persentase Realisasi (%) dari Anggaran
|
||||||
|
// </Text>
|
||||||
|
|
||||||
|
// <BarChart
|
||||||
|
// h={200}
|
||||||
|
// data={chartData}
|
||||||
|
// orientation="vertical"
|
||||||
|
// dataKey="name"
|
||||||
|
// barProps={{ radius: 6 }}
|
||||||
|
// series={[
|
||||||
|
// {
|
||||||
|
// name: 'persen',
|
||||||
|
// label: 'Persentase',
|
||||||
|
// color: colors['blue-button'],
|
||||||
|
// },
|
||||||
|
// ]}
|
||||||
|
// yAxisProps={{
|
||||||
|
// domain: [0, 100],
|
||||||
|
// }}
|
||||||
|
// valueFormatter={(v) => `${v.toFixed(1)}%`}
|
||||||
|
// />
|
||||||
|
// </Stack>
|
||||||
|
// </Paper>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default APBDesProgress;
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
|
// src/app/admin/(dashboard)/landing-page/APBDes/APBDesTable.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Box, Paper, Table, Text, Title, Badge, Group } from '@mantine/core';
|
import { Box, Paper, Table, Text, Title, Badge, Group } from '@mantine/core';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { APBDesData } from './types';
|
|
||||||
|
interface APBDesItem {
|
||||||
|
id: string;
|
||||||
|
kode: string;
|
||||||
|
uraian: string;
|
||||||
|
anggaran: number;
|
||||||
|
realisasi: number;
|
||||||
|
selisih: number;
|
||||||
|
persentase: number;
|
||||||
|
level: number;
|
||||||
|
tipe: 'pendapatan' | 'belanja' | 'pembiayaan';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APBDesData {
|
||||||
|
id: string;
|
||||||
|
tahun: number;
|
||||||
|
items: APBDesItem[];
|
||||||
|
image?: { id: string; url: string } | null;
|
||||||
|
file?: { id: string; url: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Format Rupiah, tapi jika 0 → tampilkan '-'
|
// Helper: Format Rupiah, tapi jika 0 → tampilkan '-'
|
||||||
function formatRupiahOrEmpty(value: number): string {
|
function formatRupiahOrEmpty(value: number): string {
|
||||||
@@ -29,12 +51,22 @@ function getIndent(level: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APBDesTableProps {
|
function APBDesTable() {
|
||||||
apbdesData: APBDesData;
|
const state = useProxy(apbdes);
|
||||||
}
|
const data = state.findMany.data || [];
|
||||||
|
|
||||||
function APBDesTable({ apbdesData }: APBDesTableProps) {
|
// Get the first APBDes item
|
||||||
const items = Array.isArray(apbdesData.items) ? apbdesData.items : [];
|
const apbdesItem = data[0] as unknown as APBDesData | undefined;
|
||||||
|
|
||||||
|
if (!apbdesItem) {
|
||||||
|
return (
|
||||||
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
<Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(apbdesItem.items) ? apbdesItem.items : [];
|
||||||
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
@@ -44,13 +76,13 @@ function APBDesTable({ apbdesData }: APBDesTableProps) {
|
|||||||
const totalPersentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
const totalPersentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pt={"xs"} pb="md" px={{ base: 'md', md: 100 }}>
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
<Title order={4} c={colors['blue-button']} mb="sm">
|
<Title order={4} c={colors['blue-button']} mb="sm">
|
||||||
Rincian APBDes Tahun {apbdesData.tahun}
|
Rincian APBDes Tahun {apbdesItem.tahun}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Paper withBorder radius="md" shadow="xs" p="md">
|
<Paper withBorder radius="md" shadow="xs" p="md">
|
||||||
<Box style={{ overflowY: 'auto' }}>
|
<Box style={{overflowY: 'auto' }}>
|
||||||
<Table withColumnBorders highlightOnHover>
|
<Table withColumnBorders highlightOnHover>
|
||||||
<Table.Thead bg="#2c5f78">
|
<Table.Thead bg="#2c5f78">
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
@@ -77,7 +109,9 @@ function APBDesTable({ apbdesData }: APBDesTableProps) {
|
|||||||
<Table.Td style={getIndent(item.level)}>
|
<Table.Td style={getIndent(item.level)}>
|
||||||
<Group gap="xs" align="flex-start">
|
<Group gap="xs" align="flex-start">
|
||||||
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
||||||
<Text fz="sm">{item.uraian}</Text>
|
<Text fz="sm" >
|
||||||
|
{item.uraian}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right">{formatRupiahOrEmpty(item.anggaran)}</Table.Td>
|
<Table.Td ta="right">{formatRupiahOrEmpty(item.anggaran)}</Table.Td>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export type APBDesTipe = 'pendapatan' | 'belanja' | 'pembiayaan';
|
|
||||||
|
|
||||||
export function isAPBDesTipe(tipe: string | null | undefined): tipe is APBDesTipe {
|
|
||||||
return tipe === 'pendapatan' || tipe === 'belanja' || tipe === 'pembiayaan';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APBDesItem {
|
|
||||||
id: string;
|
|
||||||
kode: string;
|
|
||||||
uraian: string;
|
|
||||||
anggaran: number;
|
|
||||||
realisasi: number;
|
|
||||||
selisih: number;
|
|
||||||
persentase: number;
|
|
||||||
level: number;
|
|
||||||
tipe?: APBDesTipe | null;
|
|
||||||
// Additional fields from API
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
deletedAt?: Date | null;
|
|
||||||
isActive?: boolean;
|
|
||||||
apbdesId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APBDesData {
|
|
||||||
id: string;
|
|
||||||
tahun: number | null;
|
|
||||||
items: APBDesItem[];
|
|
||||||
image?: { id: string; url: string } | null;
|
|
||||||
file?: { id: string; url: string } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformAPBDesData(data: any): APBDesData {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
items: data.items.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
tipe: isAPBDesTipe(item.tipe) ? item.tipe : null
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,21 +4,19 @@
|
|||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'
|
||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, Select, SimpleGrid, Stack, Text, Title } from '@mantine/core'
|
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, SimpleGrid, Stack, Text, Title } from '@mantine/core'
|
||||||
import { IconDownload } from '@tabler/icons-react'
|
import { IconDownload } from '@tabler/icons-react'
|
||||||
import { Link } from 'next-view-transitions'
|
import { Link } from 'next-view-transitions'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
|
import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
|
||||||
import APBDesTable from './lib/apbDesaTable'
|
|
||||||
import APBDesProgress from './lib/apbDesaProgress'
|
import APBDesProgress from './lib/apbDesaProgress'
|
||||||
import { transformAPBDesData } from './lib/types'
|
import APBDesTable from './lib/apbDesaTable'
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const state = useProxy(apbdes)
|
const state = useProxy(apbdes)
|
||||||
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,23 +34,6 @@ function Page() {
|
|||||||
|
|
||||||
const dataAPBDes = state.findMany.data || []
|
const dataAPBDes = state.findMany.data || []
|
||||||
|
|
||||||
// Buat daftar tahun unik dari data
|
|
||||||
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
|
|
||||||
.sort((a, b) => b - a) // urutkan descending
|
|
||||||
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
|
|
||||||
|
|
||||||
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
|
|
||||||
useEffect(() => {
|
|
||||||
if (years.length > 0 && !selectedYear) {
|
|
||||||
setSelectedYear(years[0].value)
|
|
||||||
}
|
|
||||||
}, [years, selectedYear])
|
|
||||||
|
|
||||||
// Transform and filter data based on selected year
|
|
||||||
const currentApbdes = dataAPBDes.length > 0
|
|
||||||
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -113,31 +94,8 @@ function Page() {
|
|||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */}
|
<APBDesTable />
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<APBDesProgress />
|
||||||
<Select
|
|
||||||
label="Pilih Tahun APBDes"
|
|
||||||
placeholder="Pilih tahun"
|
|
||||||
value={selectedYear}
|
|
||||||
onChange={setSelectedYear}
|
|
||||||
data={years}
|
|
||||||
w={{ base: '100%', sm: 200 }}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ada tahun tersedia"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{/* ❗ Pass currentApbdes ke komponen anak */}
|
|
||||||
{currentApbdes ? (
|
|
||||||
<>
|
|
||||||
<APBDesTable apbdesData={currentApbdes} />
|
|
||||||
<APBDesProgress apbdesData={currentApbdes} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
|
||||||
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||||
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
|
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
|
||||||
import { transformAPBDesData } from '@/app/darmasaba/(tambahan)/apbdes/lib/types'
|
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { ActionIcon, BackgroundImage, Box, Button, Center, Group, Loader, Select, SimpleGrid, Stack, Text } from '@mantine/core'
|
import { ActionIcon, BackgroundImage, Box, Button, Center, Flex, Group, Loader, SimpleGrid, Stack, Text } from '@mantine/core'
|
||||||
import { IconDownload } from '@tabler/icons-react'
|
import { IconDownload } from '@tabler/icons-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -14,7 +12,6 @@ import { useProxy } from 'valtio/utils'
|
|||||||
function Apbdes() {
|
function Apbdes() {
|
||||||
const state = useProxy(apbdes)
|
const state = useProxy(apbdes)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const textHeading = {
|
const textHeading = {
|
||||||
title: 'APBDes',
|
title: 'APBDes',
|
||||||
@@ -35,24 +32,6 @@ function Apbdes() {
|
|||||||
loadData()
|
loadData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const dataAPBDes = state.findMany.data || []
|
|
||||||
|
|
||||||
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
|
|
||||||
.sort((a, b) => b - a) // urutkan descending
|
|
||||||
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
|
|
||||||
|
|
||||||
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
|
|
||||||
useEffect(() => {
|
|
||||||
if (years.length > 0 && !selectedYear) {
|
|
||||||
setSelectedYear(years[0].value)
|
|
||||||
}
|
|
||||||
}, [years, selectedYear])
|
|
||||||
|
|
||||||
// Transform and filter data based on selected year
|
|
||||||
const currentApbdes = dataAPBDes.length > 0
|
|
||||||
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
|
|
||||||
: null
|
|
||||||
|
|
||||||
const data = (state.findMany.data || []).slice(0, 3)
|
const data = (state.findMany.data || []).slice(0, 3)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,30 +60,8 @@ function Apbdes() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */}
|
{/* Chart */}
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<APBDesProgress />
|
||||||
<Select
|
|
||||||
label="Pilih Tahun APBDes"
|
|
||||||
placeholder="Pilih tahun"
|
|
||||||
value={selectedYear}
|
|
||||||
onChange={setSelectedYear}
|
|
||||||
data={years}
|
|
||||||
w={{ base: '100%', sm: 200 }}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ada tahun tersedia"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{currentApbdes ? (
|
|
||||||
<>
|
|
||||||
<APBDesProgress apbdesData={currentApbdes} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
|
||||||
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}>
|
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -133,7 +90,7 @@ function Apbdes() {
|
|||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
||||||
<Stack gap={"xs"} justify="space-between" h="100%" p="xl" pos="relative">
|
<Stack justify="space-between" h="100%" p="xl" pos="relative">
|
||||||
<Text
|
<Text
|
||||||
c="white"
|
c="white"
|
||||||
fw={600}
|
fw={600}
|
||||||
@@ -152,20 +109,7 @@ function Apbdes() {
|
|||||||
>
|
>
|
||||||
{v.jumlah}
|
{v.jumlah}
|
||||||
</Text>
|
</Text>
|
||||||
<Center>
|
<Group justify="center">
|
||||||
<ActionIcon
|
|
||||||
component={Link}
|
|
||||||
href={v.file?.link || ''}
|
|
||||||
radius="xl"
|
|
||||||
size="xl"
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
|
||||||
>
|
|
||||||
<IconDownload size={20} color="white" />
|
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
</Center>
|
|
||||||
{/* <Group justify="center">
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={Link}
|
component={Link}
|
||||||
href={v.file?.link || ''}
|
href={v.file?.link || ''}
|
||||||
@@ -174,18 +118,18 @@ function Apbdes() {
|
|||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
||||||
>
|
>
|
||||||
<Group align="center" gap="xs" px="md" py={6}>
|
<Flex align="center" gap="xs" px="md" py={6}>
|
||||||
<IconDownload size={25} color="white" />
|
<IconDownload size={18} color="white" />
|
||||||
</Group>
|
</Flex>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group> */}
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { ActionIcon, Card, Flex, Image, Text, Tooltip } from "@mantine/core";
|
||||||
import { sosmedMap } from "@/app/admin/(dashboard)/landing-page/profil/_lib/sosmed";
|
|
||||||
import colors from "@/con/colors";
|
|
||||||
import { ActionIcon, Box, Card, Flex, Image, Text, Tooltip } from "@mantine/core";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
|
import { IconBrandInstagram, IconBrandFacebook, IconBrandTwitter, IconWorld } from "@tabler/icons-react";
|
||||||
|
|
||||||
function SosmedView({
|
function SosmedView({
|
||||||
data,
|
data,
|
||||||
@@ -12,12 +10,17 @@ function SosmedView({
|
|||||||
}) {
|
}) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
const fallbackIcon = (platform?: string) => {
|
||||||
if (item.image?.link) return item.image.link;
|
switch (platform?.toLowerCase()) {
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
case "instagram":
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
return <IconBrandInstagram size={22} />;
|
||||||
|
case "facebook":
|
||||||
|
return <IconBrandFacebook size={22} />;
|
||||||
|
case "twitter":
|
||||||
|
return <IconBrandTwitter size={22} />;
|
||||||
|
default:
|
||||||
|
return <IconWorld size={22} />;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,24 +44,18 @@ function SosmedView({
|
|||||||
boxShadow: "0 0 12px rgba(28, 110, 164, 0.6)",
|
boxShadow: "0 0 12px rgba(28, 110, 164, 0.6)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{item.image?.link ? (
|
||||||
const src = getIconSource(item);
|
<Image
|
||||||
|
src={item.image.link}
|
||||||
if (src) {
|
alt={item.name || "ikon"}
|
||||||
return (
|
w={24}
|
||||||
<Image
|
h={24}
|
||||||
loading="lazy"
|
fit="contain"
|
||||||
src={src}
|
loading="lazy"
|
||||||
alt={item.name}
|
/>
|
||||||
w={24}
|
) : (
|
||||||
h={24}
|
fallbackIcon(item.name)
|
||||||
fit="contain"
|
)}
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
|
||||||
})()}
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const getDetailUrl = (item: { type?: string; id: string | number;[key: string]: unknown }) => {
|
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||||
const { type, id, kategori } = item;
|
const { type, id, kategori } = item;
|
||||||
const map: Record<string, (id: string | number, kategori?: string) => string> = {
|
const map: Record<string, (id: string | number, kategori?: string) => string> = {
|
||||||
programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
|
programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
|
||||||
@@ -30,8 +30,6 @@ 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',
|
||||||
@@ -84,7 +82,7 @@ const getDetailUrl = (item: { type?: string; id: string | number;[key: string]:
|
|||||||
jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
|
jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||||
dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua',
|
dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua',
|
||||||
dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan',
|
dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type && map[type]) return map[type](id, kategori as string | undefined);
|
if (type && map[type]) return map[type](id, kategori as string | undefined);
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Syarat & Ketentuan Penggunaan HIPMI Badung Connect</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1e293b;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px 20px;
|
|
||||||
background-color: white;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e3a5f;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e3a5f;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #1e3a5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: 3rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 24px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Syarat & Ketentuan Penggunaan HIPMI Badung Connect</h1>
|
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
<p>Dengan menggunakan aplikasi <strong>HIPMI Badung Connect</strong> ("Aplikasi"), Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan berikut. Jika Anda tidak setuju dengan ketentuan ini, harap jangan gunakan Aplikasi.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>1. Definisi</h2>
|
|
||||||
<p><strong>HIPMI Badung Connect</strong> adalah platform digital resmi untuk anggota Himpunan Pengusaha Muda Indonesia (HIPMI) Kabupaten Badung, yang bertujuan memfasilitasi jaringan, kolaborasi, dan pertumbuhan bisnis para pengusaha muda.</p>
|
|
||||||
|
|
||||||
<h2>2. Larangan Konten Tidak Pantas</h2>
|
|
||||||
<p>Anda <strong>dilarang keras</strong> memposting, mengirim, membagikan, atau mengunggah konten apa pun yang mengandung:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)</li>
|
|
||||||
<li>Pornografi, konten seksual eksplisit, atau gambar tidak senonoh</li>
|
|
||||||
<li>Ancaman, pelecehan, bullying, atau perilaku melecehkan</li>
|
|
||||||
<li>Informasi palsu, hoaks, spam, atau konten menyesatkan</li>
|
|
||||||
<li>Konten ilegal, melanggar hukum, atau melanggar hak kekayaan intelektual pihak lain</li>
|
|
||||||
<li>Promosi narkoba, perjudian, atau aktivitas ilegal lainnya</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>3. Tanggung Jawab Pengguna</h2>
|
|
||||||
<p>Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau bagikan melalui fitur-fitur berikut:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Profil (bio, foto, portofolio)</li>
|
|
||||||
<li>Forum diskusi</li>
|
|
||||||
<li>Chat pribadi atau grup</li>
|
|
||||||
<li>Lowongan kerja, investasi, dan donasi</li>
|
|
||||||
</ul>
|
|
||||||
<p>Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa pemberitahuan.</p>
|
|
||||||
|
|
||||||
<h2>4. Tindakan terhadap Pelanggaran</h2>
|
|
||||||
<p>Jika kami menerima laporan atau menemukan konten yang melanggar ketentuan ini, kami akan:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Segera menghapus konten tersebut</li>
|
|
||||||
<li>Memberikan peringatan atau memblokir akun pengguna</li>
|
|
||||||
<li>Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang berlaku</li>
|
|
||||||
</ul>
|
|
||||||
<p>Tim kami berkomitmen untuk menanggapi laporan konten tidak pantas <strong>dalam waktu 24 jam</strong>.</p>
|
|
||||||
|
|
||||||
<h2>5. Mekanisme Pelaporan</h2>
|
|
||||||
<p>Anda dapat melaporkan konten atau pengguna yang mencurigakan melalui:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Tombol <strong>"Laporkan"</strong> di setiap posting forum atau pesan chat</li>
|
|
||||||
<li>Tombol <strong>"Blokir Pengguna"</strong> di profil pengguna</li>
|
|
||||||
</ul>
|
|
||||||
<p>Setiap laporan akan ditangani secara rahasia dan segera.</p>
|
|
||||||
|
|
||||||
<h2>6. Perubahan Ketentuan</h2>
|
|
||||||
<p>Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi terbaru akan dipublikasikan di halaman ini dengan tanggal revisi yang diperbarui.</p>
|
|
||||||
|
|
||||||
<h2>7. Kontak</h2>
|
|
||||||
<p>Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi kami di:<br>
|
|
||||||
<strong>bip.baliinteraktifperkasa@gmail.com</strong></p>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
© 2025 Bali Interaktif Perkasa. All rights reserved.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,105 +1,31 @@
|
|||||||
/* styles/globals.css */
|
/* styles/globals.css */
|
||||||
|
|
||||||
/* ===================================
|
|
||||||
1. IMPORT CSS LIBRARIES
|
|
||||||
=================================== */
|
|
||||||
@import "@mantine/carousel/styles.css";
|
|
||||||
@import "@mantine/dropzone/styles.css";
|
|
||||||
@import "@mantine/charts/styles.css";
|
|
||||||
@import "@mantine/dates/styles.css";
|
|
||||||
@import "@mantine/tiptap/styles.css";
|
|
||||||
@import "animate.css";
|
|
||||||
@import "react-simple-toasts/dist/style.css";
|
|
||||||
@import "react-simple-toasts/dist/theme/dark.css";
|
|
||||||
@import "primereact/resources/themes/lara-light-blue/theme.css";
|
|
||||||
@import "primereact/resources/primereact.min.css";
|
|
||||||
@import "primeicons/primeicons.css";
|
|
||||||
|
|
||||||
/* ===================================
|
|
||||||
2. FONT FACE - OPTIMIZED
|
|
||||||
=================================== */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'San Francisco';
|
font-family: 'San Francisco';
|
||||||
src: url('/assets/fonts/font.otf') format('opentype');
|
src: url('/assets/fonts/font.otf') format('opentype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap; /* ✅ TAMBAHKAN INI - Penting untuk PageSpeed! */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================
|
|
||||||
3. RESET & BASE STYLES
|
|
||||||
=================================== */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%; /* Prevent font scaling in landscape */
|
|
||||||
-moz-text-size-adjust: 100%;
|
|
||||||
text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'San Francisco', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
||||||
Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================
|
|
||||||
4. GLASS EFFECTS - OPTIMIZED
|
|
||||||
=================================== */
|
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
backdrop-filter: blur(40px);
|
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
|
backdrop-filter: blur(40px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
will-change: transform; /* ✅ Hardware acceleration */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass2 {
|
.glass2 {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
backdrop-filter: blur(40px);
|
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
|
backdrop-filter: blur(40px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
will-change: transform; /* ✅ Hardware acceleration */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass3 {
|
.glass3 {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
backdrop-filter: blur(40px);
|
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
will-change: transform; /* ✅ Hardware acceleration */
|
backdrop-filter: blur(40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================
|
|
||||||
5. PERFORMANCE OPTIMIZATION
|
|
||||||
=================================== */
|
|
||||||
img,
|
|
||||||
picture,
|
|
||||||
video,
|
|
||||||
canvas,
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce motion for accessibility */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
|
// Import styles of packages that you've installed.
|
||||||
|
// All packages except `@mantine/hooks` require styles imports
|
||||||
|
import "@mantine/carousel/styles.css";
|
||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
|
import '@mantine/dropzone/styles.css';
|
||||||
|
import "animate.css";
|
||||||
|
import 'react-simple-toasts/dist/style.css';
|
||||||
|
import 'react-simple-toasts/dist/theme/dark.css';
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import '@mantine/charts/styles.css';
|
||||||
|
import '@mantine/dates/styles.css';
|
||||||
|
import '@mantine/tiptap/styles.css';
|
||||||
|
import "primereact/resources/themes/lara-light-blue/theme.css";
|
||||||
|
import "primereact/resources/primereact.min.css";
|
||||||
|
import "primeicons/primeicons.css";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||||
import {
|
import {
|
||||||
@@ -8,83 +23,19 @@ import {
|
|||||||
createTheme,
|
createTheme,
|
||||||
mantineHtmlProps,
|
mantineHtmlProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Metadata, Viewport } from "next";
|
|
||||||
import { ViewTransitions } from "next-view-transitions";
|
import { ViewTransitions } from "next-view-transitions";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
// ✅ Pisahkan viewport ke export tersendiri
|
export const metadata = {
|
||||||
export const viewport: Viewport = {
|
title: "Desa Darmasaba",
|
||||||
width: "device-width",
|
description: "Desa Darmasaba Kabupaten Badung",
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
// ✅ Tambahkan metadataBase
|
|
||||||
metadataBase: new URL("https://cld-dkr-staging-desa-darmasaba.wibudev.com"),
|
|
||||||
|
|
||||||
title: {
|
|
||||||
default: "Desa Darmasaba",
|
|
||||||
template: "%s | Desa Darmasaba",
|
|
||||||
},
|
|
||||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
|
||||||
// ❌ HAPUS viewport dari sini
|
|
||||||
keywords: [
|
|
||||||
"desa darmasaba",
|
|
||||||
"darmasaba",
|
|
||||||
"badung",
|
|
||||||
"bali",
|
|
||||||
"desa",
|
|
||||||
"pemerintah desa",
|
|
||||||
"layanan publik",
|
|
||||||
"abang batan desa",
|
|
||||||
],
|
|
||||||
authors: [{ name: "Pemerintah Desa Darmasaba" }],
|
|
||||||
creator: "Desa Darmasaba",
|
|
||||||
publisher: "Desa Darmasaba",
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
"max-video-preview": -1,
|
|
||||||
"max-image-preview": "large",
|
|
||||||
"max-snippet": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
|
||||||
icon: "/assets/images/darmasaba-icon.png",
|
|
||||||
apple: "/assets/images/darmasaba-icon.png",
|
|
||||||
},
|
|
||||||
manifest: "/manifest.json",
|
|
||||||
openGraph: {
|
|
||||||
type: "website",
|
|
||||||
locale: "id_ID",
|
|
||||||
url: "https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
|
||||||
siteName: "Desa Darmasaba",
|
|
||||||
title: "Desa Darmasaba - Kabupaten Badung, Bali",
|
|
||||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: "/assets/images/darmasaba-icon.png",
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: "Desa Darmasaba",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
category: "government",
|
|
||||||
other: {
|
|
||||||
"msapplication-TileColor": "#ffffff",
|
|
||||||
"theme-color": "#ffffff",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
fontFamily: "San Francisco, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
|
fontFamily:
|
||||||
fontFamilyMonospace: "SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
"San Francisco, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
fontFamilyMonospace:
|
||||||
|
"SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace",
|
||||||
headings: { fontFamily: "San Francisco, sans-serif" },
|
headings: { fontFamily: "San Francisco, sans-serif" },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,23 +46,26 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ViewTransitions>
|
<ViewTransitions>
|
||||||
<html lang="id" {...mantineHtmlProps}>
|
<html lang="en" {...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<ColorSchemeScript />
|
<ColorSchemeScript />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="/assets/images/darmasaba-icon.png"
|
||||||
|
sizes="any"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider theme={theme}>
|
||||||
{children}
|
{children}
|
||||||
<LoadDataFirstClient />
|
|
||||||
<ToastContainer
|
|
||||||
position="bottom-center"
|
|
||||||
hideProgressBar
|
|
||||||
style={{ zIndex: 9999 }}
|
|
||||||
/>
|
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
|
<ToastContainer position="bottom-center" hideProgressBar style={{
|
||||||
|
zIndex: 9999
|
||||||
|
}} />
|
||||||
</body>
|
</body>
|
||||||
|
<LoadDataFirstClient />
|
||||||
</html>
|
</html>
|
||||||
</ViewTransitions>
|
</ViewTransitions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Box, Container, Divider, List, ListItem, Paper, Stack, Text, Title } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Container size="md" py={40}>
|
|
||||||
<Stack gap="xl">
|
|
||||||
<Title order={1} size="h1" fw={700} c="blue.9">
|
|
||||||
Syarat & Ketentuan Penggunaan Admin Desa Darmasaba
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Paper p="lg" radius="md" withBorder bg="gray.0" style={{ borderLeft: '4px solid #1e3a5f' }}>
|
|
||||||
<Text c="gray.8">
|
|
||||||
Dengan menggunakan website <Text component="span" fw={600}>Admin Desa Darmasaba</Text> ("Website"),
|
|
||||||
Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan berikut. Jika Anda tidak setuju
|
|
||||||
dengan ketentuan ini, harap jangan gunakan Website.
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
1. Definisi
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
<Text component="span" fw={600}>Admin Desa Darmasaba</Text> adalah website resmi untuk Admin Desa Darmasaba, yang bertujuan
|
|
||||||
menambahkan, menghapus, dan mengedit konten desa ke dalam website.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
2. Larangan Konten Tidak Pantas
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7" mb="md">
|
|
||||||
Anda <Text component="span" fw={600}>dilarang keras</Text> menambahkan, menghapus, dan mengedit konten desa apa pun yang mengandung:
|
|
||||||
</Text>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)</ListItem>
|
|
||||||
<ListItem>Pornografi, konten seksual eksplisit, atau gambar tidak senonoh</ListItem>
|
|
||||||
<ListItem>Ancaman, pelecehan, bullying, atau perilaku melecehkan</ListItem>
|
|
||||||
<ListItem>Informasi palsu, hoaks, spam, atau konten menyesatkan</ListItem>
|
|
||||||
<ListItem>Konten ilegal, melanggar hukum, atau melanggar hak kekayaan intelektual pihak lain</ListItem>
|
|
||||||
<ListItem>Promosi narkoba, perjudian, atau aktivitas ilegal lainnya</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
3. Tanggung Jawab Pengguna
|
|
||||||
</Title>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau bagikan.</ListItem>
|
|
||||||
<ListItem>Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa pemberitahuan.</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
4. Tindakan terhadap Pelanggaran
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7" mb="md">
|
|
||||||
Jika kami menerima laporan atau menemukan konten yang melanggar ketentuan ini, kami akan:
|
|
||||||
</Text>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Segera menghapus konten tersebut</ListItem>
|
|
||||||
<ListItem>Menghapus akun pengguna</ListItem>
|
|
||||||
<ListItem>Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang berlaku</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
5. Perubahan Ketentuan
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi terbaru akan dipublikasikan di
|
|
||||||
halaman ini dengan tanggal revisi yang diperbarui.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
6. Kontak
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi kami di:
|
|
||||||
</Text>
|
|
||||||
<Text c="gray.7" fw={600} mt="xs">
|
|
||||||
bip.baliinteraktifperkasa@gmail.com
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider my="xl" />
|
|
||||||
|
|
||||||
<Text ta="center" c="gray.6" size="sm">
|
|
||||||
© 2025 Bali Interaktif Perkasa. All rights reserved.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -16,9 +16,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
||||||
|
|
||||||
async function fetchUser() {
|
async function fetchUser() {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/me');
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||||
@@ -34,7 +32,6 @@ export default function WaitingRoom() {
|
|||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
const MAX_RETRIES = 2;
|
const MAX_RETRIES = 2;
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
@@ -80,7 +77,7 @@ export default function WaitingRoom() {
|
|||||||
|
|
||||||
// Force a session refresh
|
// Force a session refresh
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/refresh-session', {
|
const res = await fetch('/api/refresh-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user