Compare commits

..

8 Commits

Author SHA1 Message Date
4a5524ce88 API & UI Kesehatan Sudah Sampai Di Penanganan Darurat 2025-06-20 00:08:13 +08:00
899883ca2a Fix UI & API Admin Kesehatan Puskesmas 2025-06-19 15:40:27 +08:00
10ecc13ad7 API All Kesehatan 2025-06-19 14:12:57 +08:00
58f538425c UI & API Admin Menu Kesehatan 2025-06-19 10:24:50 +08:00
d2f53ff69b Keperluan Deploy 2025-06-18 16:37:08 +08:00
40f0294595 Tambahan buat deploy 2025-06-18 16:32:19 +08:00
6ed0246cea API Profile Desa Udah Clear, API Menu desa udah clear
API & UI Profile Desa Clear
2025-06-18 15:32:06 +08:00
af726043bd API Profile Desa aman, tinggal Profil Perbekel
UI Profile Desa aman, tinggal Profil Perbekel
2025-06-18 14:08:02 +08:00
100 changed files with 5724 additions and 1330 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -23,7 +23,7 @@
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
"@mantine/dates": "^8.1.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
"@mantine/tiptap": "^7.17.4",

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"judul": "Lambang Desa",
"deskripsi" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>"
}

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"judul": "Maskot Desa",
"deskripsi" : "<p>Pudak adalah bunga dari tanaman sejenis pandan (Pandanaceae). Bentuk bunga ini tersusun dalam beberapa lapisan, terbungkus oleh kelopak warna putih (semacam daun lonjong) yang ujungnya meruncing.</p><p>Bunga Pudak berwarna kuning dan akan terlihat jika kelopak atau pelepahnya telah mekar. Kekhasan dari bunga pudak, yaitu mempunyai aroma wangi yang semerbak nan lembut (tidak menyengat), dan dapat menebar keharuman sepanjang pagi atau pun sore hari. Tanaman ini dapat tumbuh di sepanjang pantai, aliran sungai, di atas batu-batu karang, dan juga di tanah ladang.</p><p>Dalam Kamus Jawa Kuna- Indonesia kata “Pudak” berarti bunga pandan atau Pandanus Moschatus (Mardiwarsito: 1981: 442). Selain itu bunga pudak juga dapat disebut ketaka atau ketaki (Mardiwarsito, 1981: 276). Sedangkan kata “Sategal” berasal dari kata dasar “Tegal” yang berarti ladang (Mardiwarsito, 1981: 593). Jadi Pudak Sategal dapat diartikan sebagai satu ladang luas yang dipenuhi bunga pudak dan menabar keharuman.</p><p>Pada sebuah kesempatan, Ida Pedanda Putu Pemaron menjelaskan mengenai makna dari istilah Pudak Sategal dengan sebuah analogi bahwa, sekuntum bunga pudak memiliki aroma wangi atau keharuman yang sangat kuat, apalagi jika satu ladang penuh bunga pudak, maka dapat dipastikan aroma keharumannya akan membumbung menyebar ke segala penjuru (Wawancara, 18 Mei 2019 di Geria Putra Mandara Kenderan, Tegallalang). “Pudak” ialah sebuah bunga yang memiliki aroma wangi atau keharuman yang semerbak, lembut, dan khas.</p><p>Garapan Tari Maskot Desa Darmasaba Sekar Pudak diwujudkan ke dalam bentuk tari kreasi yang ditarikan secara berkelompok dengan jumlah lima orang penari perempuan (putri).</p><p>Pemilihan penari perempuan dimaksudkan untuk mempresentasikan keindahan, keluwesan, dan keharuman dari bunga pudak. Sedangkan penetapan jumlah penari lima orang didasarkan atas pertimbangan kebutuhan koreografi agar dapat membentuk desain-desain komposisi lantai yang menarik dan dinamis, baik ketika ditarikan di area panggung yang luas atau pun area panggung yang kecil. Penyajian tari maskot ini dirancang dengan durasi waktu 9 menit.</p>"
}

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"biodata": "<p>I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.</p>",
"pengalaman": "<ul><li>2021 - 2027: Perbekel Desa Darmasaba</li><li>2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates</li><li>2020 - Sekarang: Founder Ugawa Record Music Studio</li><li>2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar</li></ul>",
"pengalamanOrganisasi": "<ul> <li>1996 1997: Ketua OSIS SMP Negeri 1 Abiansemal</li><li>1999 2000: Ketua OSIS SMA Negeri 1 Mengwi</li> <li>2008 2009: Ketua BEM Universitas Mahasaraswati Denpasar</li> <li>2008 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba</li> <li>2020 Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar</li> <li>2021 Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung</li> <li>2023 2028: Komite Tetap Advokasi Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung</li> </ul>",

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"judul": "Sejarah Desa",
"deskripsi": "<p>Asal usul nama Darmasaba tertuang dalam lontar Usada Bali. Seperti di tulis dalam monografi Desa Darmasaba tahun 1980 silam, nama Darmasaba berkaitan dengan keturunan Danghyang Nirarta diceritakan, Sang kawi-wiku asal Daha (Jawa Timur) itu memiliki cucu bernama Ida Pedanda Sakti Manuaba yang tigggal di Desa Kendran Tegalalang Gianyar. Merasa tidak disenangi sang ayah, Ida Pedanda Sakti Manuaba pergi mengembara bersama dua orang pengiringnya. Pengembaraan sang pendeta sampai di pura Sarin Buana di Jimbaran. Saat mengadakan semedi di tempat ini sang pendeta melihat sinar api. Yang sangat jauh di utara. Timbul keinginan Ida Pedanda Manuaba untuk mengunjungi tempat itu. Sampailah sang Pedanda di pura Batan Bila Peguyangan. Disini Ida Pedanda Manuaba singgah menghadap Ida Pedanda Budha yang tinggal disana. Selanjutnya, kedua pendeta bersama-sama menuju arah utara dan singgah di Taman Cang Ana, sebuah taman milik Arya Lanang Blusung. Di tempat ini kedua pendeta bersama-sama melaksanakan semedi dan menetap untuk sementara waktu.</p>"
}

View File

@@ -1,6 +1,6 @@
[
{
"id" : "1",
"id" : "edit",
"visi" : "<p>Mewujudkan Desa Darmasaba yang sejahtera, unggul, religius, berbudaya, dan aman dengan berlandaskan Tri Hita Karana</p>",
"misi" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>"
}

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"biodata": "<p>I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.</p>",
"riwayat": "<ul> <li>2021 - 2027: Perbekel Desa Darmasaba</li> <li>2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates</li> <li>2020 - Sekarang: Founder Ugawa Record Music Studio</li> <li>2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar</li> </ul>",

View File

@@ -50,26 +50,32 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
PelayananSuratKeterangan PelayananSuratKeterangan[]
Penghargaan Penghargaan[]
ProfileDesaImage ProfileDesaImage[]
ProfilePPID ProfilePPID[]
ProfilPerbekel ProfilPerbekel[]
Penghargaan Penghargaan[]
ProfileDesaImage ProfileDesaImage[]
ProfilePPID ProfilePPID[]
ProfilPerbekel ProfilPerbekel[]
Puskesmas Puskesmas[]
ProgramKesehatan ProgramKesehatan[]
PenangananDarurat PenangananDarurat[]
KontakDarurat KontakDarurat[]
InfoWabahPenyakit InfoWabahPenyakit[]
}
//========================================= MENU PPID ========================================= //
@@ -116,6 +122,7 @@ model ProfilePPID {
riwayat String @db.Text
pengalaman String @db.Text
unggulan String @db.Text
imageUrl String?
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
@@ -278,37 +285,37 @@ model LambangDesa {
}
model MaskotDesa {
id String @id @default(cuid())
id String @id @default(cuid())
judul String
deskripsi String @db.Text
deskripsi String @db.Text
images ProfileDesaImage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProfileDesaImage {
id String @id @default(cuid())
label String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
MaskotDesa MaskotDesa @relation(fields: [maskotDesaId], references: [id])
id String @id @default(cuid())
label String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
MaskotDesa MaskotDesa @relation(fields: [maskotDesaId], references: [id])
maskotDesaId String
}
model ProfilPerbekel {
id String @id @default(cuid())
biodata String @db.Text
pengalaman String @db.Text
pengalamanOrganisasi String @db.Text
programUnggulan String @db.Text
id String @id @default(cuid())
biodata String @db.Text
pengalaman String @db.Text
pengalamanOrganisasi String @db.Text
programUnggulan String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BERITA ========================================= //
@@ -722,3 +729,98 @@ model Posyandu {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PUSKESMAS ========================================= //
model Puskesmas {
id String @id @default(cuid())
name String
alamat String
jam JamOperasional @relation(fields: [jamId], references: [id])
jamId String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
kontak KontakPuskesmas @relation(fields: [kontakId], references: [id])
kontakId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JamOperasional {
id String @id @default(cuid())
workDays String
weekDays String
holiday String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Puskesmas Puskesmas[]
}
model KontakPuskesmas {
id String @id @default(cuid())
kontakPuskesmas String
email String
facebook String
kontakUGD String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Puskesmas Puskesmas[]
}
// ========================================= PROGRAM KESSEHATAN ========================================= //
model ProgramKesehatan {
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENANGANAN DARURAT ========================================= //
model PenangananDarurat {
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KONTAK DARURAT ========================================= //
model KontakDarurat {
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO WABAH PENYAKIT ========================================= //
model InfoWabahPenyakit {
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsiLengkap String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

BIN
public/bungapudak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
public/klimakstari.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
public/pohonpudak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

BIN
public/tarisekar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(5000),
kategori: z.string().min(1).max(50),
imageId: z.string().min(1).max(50),
content: z.string().min(1).max(5000),

View File

@@ -38,13 +38,13 @@ const sejarahDesa = proxy({
this.error = null;
try {
const response = await fetch(`/api/desa/profile/sejarah-desa/${id}`);
const response = await fetch(`/api/desa/profile/sejarah/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.data = result.data;
return result.data;
@@ -91,64 +91,69 @@ const sejarahDesa = proxy({
},
async submit() {
// Validate form
const validation = sejarahDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
// Validate form
const validation = sejarahDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/sejarah/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/desa/profile/sejarah-desa/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update profile");
// Refresh profile data
await sejarahDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update profile");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update profile error:", errorMessage);
toast.error("Terjadi kesalahan saat update profile");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...sejarahDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update profile");
// Refresh profile data
await sejarahDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update profile");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update profile error:", errorMessage);
toast.error("Terjadi kesalahan saat update profile");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...sejarahDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
});
// ========================================= VISI MISI DESA ========================================= //
@@ -187,12 +192,12 @@ const visiMisiDesa = proxy({
try {
const response = await fetch(`/api/desa/profile/visi-misi/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.data = result.data;
return result.data;
@@ -239,64 +244,66 @@ const visiMisiDesa = proxy({
},
async submit() {
// Validate form
const validation = visiMisiDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/desa/profile/visi-misi/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update visi misi desa");
// Refresh profile data
await visiMisiDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update visi misi desa");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update visi misi desa error:", errorMessage);
toast.error("Terjadi kesalahan saat update visi misi desa");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...visiMisiDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
// Validate form
const validation = visiMisiDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/desa/profile/visi-misi/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update visi misi desa");
// Refresh profile data
await visiMisiDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update visi misi desa");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update visi misi desa error:", errorMessage);
toast.error("Terjadi kesalahan saat update visi misi desa");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...visiMisiDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
});
// ========================================= LAMBANG DESA ========================================= //
@@ -334,13 +341,13 @@ const lambangDesa = proxy({
this.error = null;
try {
const response = await fetch(`/api/desa/profile/lambang-desa/${id}`);
const response = await fetch(`/api/desa/profile/lambang/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.data = result.data;
return result.data;
@@ -387,64 +394,69 @@ const lambangDesa = proxy({
},
async submit() {
// Validate form
const validation = lambangDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
// Validate form
const validation = lambangDesaForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/lambang/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/desa/profile/lambang-desa/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update lambang desa");
// Refresh profile data
await lambangDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update lambang desa");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update lambang desa error:", errorMessage);
toast.error("Terjadi kesalahan saat update lambang desa");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...lambangDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update lambang desa");
// Refresh profile data
await lambangDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update lambang desa");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update lambang desa error:", errorMessage);
toast.error("Terjadi kesalahan saat update lambang desa");
return false;
} finally {
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...lambangDesaDefaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
});
// ========================================= MASKOT DESA ========================================= //
@@ -502,7 +514,7 @@ const maskotDesa = proxy({
this.error = null;
try {
const response = await fetch(`/api/desa/profile/maskot-desa/${id}`);
const response = await fetch(`/api/desa/profile/maskot/${id}`);
const result = await response.json();
if (response.ok && result.success) {
@@ -577,7 +589,7 @@ const maskotDesa = proxy({
try {
const response = await fetch(
`/api/desa/profile/maskot-desa/${this.id}`,
`/api/desa/profile/maskot/${this.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -678,7 +690,7 @@ const profilPerbekel = proxy({
this.error = null;
try {
const response = await fetch(`/api/desa/profile/profil-perbekel/${id}`);
const response = await fetch(`/api/desa/profile/profileperbekel/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -750,7 +762,7 @@ const profilPerbekel = proxy({
try {
const response = await fetch(
`/api/desa/profile/profil-perbekel/${this.id}`,
`/api/desa/profile/profileperbekel/${this.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -806,11 +818,12 @@ const profilPerbekel = proxy({
},
});
const stateProfileDesa = {
const stateProfileDesa = proxy({
lambangDesa,
maskotDesa,
profilPerbekel,
visiMisiDesa,
sejarahDesa,
};
});
export default stateProfileDesa;

View File

@@ -0,0 +1,208 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
})
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
}
const penangananDarurat = proxy({
findMany: {
data: [] as Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.penanganandarurat[
"find-many"
].get();
if (res.status === 200) {
penangananDarurat.findMany.data = res.data?.data ?? [];
}
},
},
create:{
form: {...defaultForm},
loading: false,
async create() {
const cek = templateForm.safeParse(penangananDarurat.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
penangananDarurat.create.loading = true;
const res = await ApiFetch.api.kesehatan.penanganandarurat[
"create"
].post(penangananDarurat.create.form);
if (res.status === 200) {
penangananDarurat.findMany.load();
return toast.success("Penanganan Darurat berhasil disimpan!");
}
return toast.error("Gagal menyimpan penanganan darurat");
} catch (error) {
console.log((error as Error).message);
} finally {
penangananDarurat.create.loading = false;
}
},
resetForm() {
penangananDarurat.create.form = {...defaultForm};
}
},
findUnique: {
data: null as Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/penanganandarurat/${id}`);
if (res.ok) {
const data = await res.json();
penangananDarurat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
penangananDarurat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
penangananDarurat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
try {
penangananDarurat.delete.loading = true;
const response = await fetch(`/api/kesehatan/penanganandarurat/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Penanganan darurat berhasil dihapus");
await penangananDarurat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus penanganan darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus penanganan darurat");
} finally {
penangananDarurat.delete.loading = false;
}
}
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/penanganandarurat/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching penanganan darurat:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(penangananDarurat.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
penangananDarurat.edit.loading = true;
const response = await fetch(`/api/kesehatan/penanganandarurat/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Penanganan darurat berhasil diupdate");
await penangananDarurat.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update penanganan darurat");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate penanganan darurat");
return false;
} finally {
penangananDarurat.edit.loading = false;
}
},
reset() {
penangananDarurat.edit.id = "";
penangananDarurat.edit.form = { ...defaultForm };
},
},
});
export default penangananDarurat

View File

@@ -0,0 +1,217 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, { message: "Name is required" }),
nomor: z.string().min(1, { message: "Nomor is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
nomor: "",
deskripsi: "",
imageId: "",
};
const posyandustate = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(posyandustate.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
posyandustate.create.loading = true;
const res = await ApiFetch.api.kesehatan.posyandu["create"].post(posyandustate.create.form);
if (res.status === 200) {
posyandustate.findMany.load();
return toast.success("Posyandu berhasil disimpan!");
}
return toast.error("Gagal menyimpan posyandu");
} catch (error) {
console.log((error as Error).message);
} finally {
posyandustate.create.loading = false;
}
},
resetForm(){
posyandustate.create.form = { ...defaultForm };
}
},
findMany: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
image: true;
}
}>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get();
if (res.status === 200) {
posyandustate.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
image: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/posyandu/${id}`);
if (res.ok) {
const data = await res.json();
posyandustate.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posyandu:", res.statusText);
posyandustate.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posyandu:", error);
posyandustate.findUnique.data = null;
}
}
},
delete: {
loading: false,
async byId(id: string) {
try {
posyandustate.delete.loading = true;
const response = await fetch(`/api/kesehatan/posyandu/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Posyandu berhasil dihapus");
await posyandustate.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus posyandu");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus posyandu");
} finally {
posyandustate.delete.loading = false;
}
}
},
edit: {
id: "",
form: {...defaultForm},
loading: false,
async load(id: string) {
if(!id){
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/posyandu/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if(!response.ok){
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if(result?.success){
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
nomor: data.nomor,
deskripsi: data.deskripsi,
imageId: data.imageId || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching posyandu:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(posyandustate.edit.form);
if(!cek.success){
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
posyandustate.edit.loading = true;
const response = await fetch(`/api/kesehatan/posyandu/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: this.form.name,
nomor: this.form.nomor,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
if(!response.ok){
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if(result.success){
toast.success(result.message || "Posyandu berhasil diperbarui");
await posyandustate.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching posyandu:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return false;
} finally {
posyandustate.edit.loading = false;
}
},
reset() {
posyandustate.edit.id = "";
posyandustate.edit.form = {...defaultForm};
}
}
})
export default posyandustate;

View File

@@ -0,0 +1,214 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
deskripsiSingkat: "",
deskripsi: "",
imageId: "",
};
const programKesehatan = proxy({
findMany: {
data: [] as Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get();
if (res.status === 200) {
programKesehatan.findMany.data = res.data?.data ?? [];
}
},
},
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKesehatan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKesehatan.create.loading = true;
const res = await ApiFetch.api.kesehatan.programkesehatan[
"create"
].post(programKesehatan.create.form);
if (res.status === 200) {
programKesehatan.findMany.load();
return toast.success("Program Kesehatan berhasil disimpan!");
}
return toast.error("Gagal menyimpan program kesehatan");
} catch (error) {
console.log((error as Error).message);
} finally {
programKesehatan.create.loading = false;
}
},
resetForm() {
programKesehatan.create.form = { ...defaultForm };
},
},
findUnique: {
data: null as Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/programkesehatan/${id}`);
if (res.ok) {
const data = await res.json();
programKesehatan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program kesehatan:", res.statusText);
programKesehatan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program kesehatan:", error);
programKesehatan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKesehatan.delete.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program kesehatan berhasil dihapus");
await programKesehatan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kesehatan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kesehatan");
} finally {
programKesehatan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/programkesehatan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsiSingkat: data.deskripsiSingkat,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching program kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKesehatan.edit.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Program kesehatan berhasil diupdate");
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
return false;
} finally {
programKesehatan.edit.loading = false;
}
},
reset() {
programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm };
},
},
});
export default programKesehatan;

View File

@@ -0,0 +1,298 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// Validasi form
const templateForm = z.object({
name: z.string().min(1),
alamat: z.string().min(1),
imageId: z.string().min(1),
jam: z.object({
workDays: z.string().min(1),
weekDays: z.string().min(1),
holiday: z.string().min(1),
}),
kontak: z.object({
kontakPuskesmas: z.string().min(1),
email: z.string().min(1),
facebook: z.string().min(1),
kontakUGD: z.string().min(1),
}),
});
// Default form
const defaultForm = {
name: "",
alamat: "",
imageId: "",
jam: {
workDays: "",
weekDays: "",
holiday: "",
},
kontak: {
kontakPuskesmas: "",
email: "",
facebook: "",
kontakUGD: "",
},
image: undefined,
};
const puskesmasState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(puskesmasState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
return toast.error(err);
}
try {
puskesmasState.create.loading = true;
console.log('Form data:', puskesmasState.create.form);
interface ErrorResponse {
message?: string;
error?: string;
errors?: Record<string, string[]>;
}
const payload = {
name: puskesmasState.create.form.name,
alamat: puskesmasState.create.form.alamat,
imageId: puskesmasState.create.form.imageId,
jam: {
workDays: puskesmasState.create.form.jam.workDays,
weekDays: puskesmasState.create.form.jam.weekDays,
holiday: puskesmasState.create.form.jam.holiday,
},
kontak: {
kontakPuskesmas: puskesmasState.create.form.kontak.kontakPuskesmas,
email: puskesmasState.create.form.kontak.email,
facebook: puskesmasState.create.form.kontak.facebook,
kontakUGD: puskesmasState.create.form.kontak.kontakUGD,
},
};
console.log('Sending payload:', JSON.stringify(payload, null, 2));
try {
const res = await ApiFetch.api.kesehatan.puskesmas.create.post(payload);
console.log('API Response:', res);
if (res.status === 200) {
await puskesmasState.findMany.load();
toast.success("Berhasil menambahkan puskesmas");
return res;
} else {
console.error('API Error Response:', {
status: res.status,
data: res.data
});
const errorData = res.data as ErrorResponse;
let errorMessage = 'Gagal menambahkan puskesmas';
if (errorData?.message) {
errorMessage = errorData.message;
} else if (errorData?.error) {
errorMessage = errorData.error;
} else if (errorData?.errors) {
errorMessage = Object.entries(errorData.errors)
.map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`)
.join('; ');
}
console.error('Extracted error message:', errorMessage);
toast.error(errorMessage);
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error in API call:', {
error,
errorString: String(error),
jsonError: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack
} : 'Not an Error instance'
});
throw error;
}
} catch (error) {
console.error("Error in puskesmas submit:", {
error,
errorString: String(error),
errorType: typeof error,
isErrorInstance: error instanceof Error,
errorDetails: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause
} : null
});
let errorMessage = "Terjadi kesalahan saat menambahkan puskesmas";
if (error instanceof Error) {
errorMessage = error.message || errorMessage;
} else if (error && typeof error === 'object' && 'message' in error) {
errorMessage = String((error as { message: unknown }).message);
} else if (typeof error === 'string') {
errorMessage = error;
}
console.error('Displaying error to user:', errorMessage);
toast.error(errorMessage);
throw error;
} finally {
puskesmasState.create.loading = false;
}
},
resetForm() {
puskesmasState.create.form = { ...defaultForm };
}
},
findMany: {
data: null as Prisma.PuskesmasGetPayload<{
include: { image: true; jam: true; kontak: true };
}>[] | null,
async load() {
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get();
if (res.status === 200) {
puskesmasState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PuskesmasGetPayload<{
include: { image: true; jam: true; kontak: true };
}> | null,
async load(id: string) {
const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
if (res.ok) {
const data = await res.json();
puskesmasState.findUnique.data = data.data ?? null;
} else {
toast.error("Gagal load data puskesmas");
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
if (!res.ok) {
toast.error("Gagal memuat data");
return;
}
const result = await res.json();
const data = result.data;
puskesmasState.edit.id = data.id;
puskesmasState.edit.form = {
name: data.name,
alamat: data.alamat,
imageId: data.imageId,
jam: {
workDays: data.jam.workDays,
weekDays: data.jam.weekDays,
holiday: data.jam.holiday,
},
kontak: {
kontakPuskesmas: data.kontak.kontakPuskesmas,
email: data.kontak.email,
facebook: data.kontak.facebook,
kontakUGD: data.kontak.kontakUGD,
},
image: data.image,
};
},
async submit() {
const cek = templateForm.safeParse(puskesmasState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
toast.error(err);
return false;
}
try {
puskesmasState.edit.loading = true;
const payload = {
name: puskesmasState.edit.form.name,
alamat: puskesmasState.edit.form.alamat,
imageId: puskesmasState.edit.form.imageId,
jam: { ...puskesmasState.edit.form.jam },
kontak: { ...puskesmasState.edit.form.kontak }
};
const res = await fetch(`/api/kesehatan/puskesmas/${puskesmasState.edit.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || "Update gagal");
}
toast.success("Berhasil update puskesmas");
await puskesmasState.findMany.load();
return true;
} catch (err) {
toast.error(err instanceof Error ? err.message : "Terjadi kesalahan saat update");
return false;
} finally {
puskesmasState.edit.loading = false;
}
},
reset() {
puskesmasState.edit.id = "";
puskesmasState.edit.form = { ...defaultForm };
}
},
delete: {
loading: false,
async byId(id: string) {
try {
puskesmasState.delete.loading = true;
const res = await fetch(`/api/kesehatan/puskesmas/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result.success) {
toast.success("Puskesmas berhasil dihapus");
await puskesmasState.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus");
}
} catch {
toast.error("Terjadi kesalahan saat menghapus");
} finally {
puskesmasState.delete.loading = false;
}
}
}
});
export default puskesmasState;

View File

@@ -5,9 +5,9 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikJenisKelamin = z.object({
laki: z.string().min(2, "Data laki-laki harus diisi"),
perempuan: z.string().min(2, "Data perempuan harus diisi"),
});
laki: z.string().min(1, "Data laki-laki harus diisi"),
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
type GrafikJenisKelamin = Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
select: {

View File

@@ -5,10 +5,10 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikUmur = z.object({
remaja: z.string().min(2, "Data remaja harus diisi"),
dewasa: z.string().min(2, "Data dewasa harus diisi"),
orangtua: z.string().min(2, "Data orangtua harus diisi"),
lansia: z.string().min(2, "Data lansia harus diisi"),
remaja: z.string().min(1, "Data remaja harus diisi"),
dewasa: z.string().min(1, "Data dewasa harus diisi"),
orangtua: z.string().min(1, "Data orangtua harus diisi"),
lansia: z.string().min(1, "Data lansia harus diisi"),
});
type GrafikUmur = Prisma.GrafikBerdasarkanUmurGetPayload<{

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
const templateGrafikHasilKepuasanMasyarakat = z.object({
label: z.string().min(2, "Label harus diisi"),
kepuasan: z.string().min(2, "Kepuasan harus diisi"),
kepuasan: z.string().min(1, "Kepuasan harus diisi"),
});
type GrafikHasilKepuasanMasyarakat = Prisma.IndeksKepuasanMasyarakatGetPayload<{

View File

@@ -125,16 +125,6 @@ function EditBerita() {
placeholder="masukkan judul"
/>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<TextInput
value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
@@ -174,6 +164,16 @@ function EditBerita() {
/>
</Box>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<Button onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>

View File

@@ -63,9 +63,9 @@ export default function CreateBerita() {
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
@@ -79,8 +79,9 @@ export default function CreateBerita() {
placeholder="masukkan judul"
/>
<SelectCategory
value={beritaState.berita.create.form.kategoriBeritaId}
onChange={(val) => {
beritaState.berita.create.form.kategoriBeritaId = val.id;
beritaState.berita.create.form.kategoriBeritaId = val?.id || "";
}}
/>
<TextInput
@@ -114,10 +115,10 @@ export default function CreateBerita() {
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
beritaState.berita.create.form.content = htmlContent;
}}
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
beritaState.berita.create.form.content = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
@@ -126,26 +127,37 @@ export default function CreateBerita() {
</Box>
);
function SelectCategory({
onChange,
}: {
interface SelectCategoryProps {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}>) => void;
}) {
}> | null) => void;
value?: string | null;
defaultValue?: string | null;
}
function SelectCategory({
onChange,
value,
defaultValue,
}: SelectCategoryProps) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load();
categoryState.findMany.load().then(() => {
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
});
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
@@ -154,13 +166,19 @@ export default function CreateBerita() {
label: item.name,
value: item.id,
}))}
onChange={(val) => {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
value={selectedValue || null}
onChange={(val: string | null) => {
if (val) {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
} else {
onChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
);

View File

@@ -1,39 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function LambangDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Lambang Desa</Title>
<Text fw={"bold"}>Deskripsi Lambang Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Lambang Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default LambangDesa;

View File

@@ -1,13 +0,0 @@
'use client'
import LayoutTabsEdit from "../_lib/layoutTabsEdit"
function Layout({children}: {children: React.ReactNode}) {
return (
<LayoutTabsEdit>
{children}
</LayoutTabsEdit>
);
}
export default Layout;

View File

@@ -1,39 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function MaskotDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Maskot Desa</Title>
<Text fw={"bold"}>Deskripsi Maskot Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Maskot Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default MaskotDesa;

View File

@@ -1,49 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, TextInput, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function ProfilePerbekel() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Profil Perbekel</Title>
<TextInput
label="Nama Perbekel"
placeholder="masukkan nama perbekel"
/>
<Text fz={"sm"} fw={"bold"}>Biodata</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Pengalaman</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Pengalaman Organisasi</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Program Unggulan</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Profil Perbekel</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default ProfilePerbekel;

View File

@@ -1,34 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
function SejarahDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Sejarah Desa</Title>
<Text fw={"bold"}>Deskripsi Sejarah Desa</Text>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default SejarahDesa;

View File

@@ -1,25 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack } from '@mantine/core';
function Page() {
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
{/* <Box px={{ base: 0, md: 30 }}>
<Text ta={"center"} fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }} />
</Box> */}
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
);
}
export default Page;

View File

@@ -1,64 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { DesaEditor } from '../../../_com/desaEditor';
function VisiMisiDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Visi Desa</Title>
<Text fw={"bold"}>Deskripsi Visi Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Visi Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
<SimpleGrid py={30} cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Misi Desa</Title>
<Text fw={"bold"}>Deskripsi Misi Desa</Text>
<DesaEditor showSubmit={false}/>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Misi Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default VisiMisiDesa;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const lambangState = useProxy(stateProfileDesa.lambangDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await lambangState.findUnique.load(id);
if (data) {
lambangState.update.initialize(data);
}
};
loadData();
return () => {
lambangState.update.reset();
lambangState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !lambangState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await lambangState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update lambang desa:", error);
toast.error("Terjadi kesalahan saat update lambang desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
lambangState.findUnique.loading ||
!lambangState.findUnique.data ||
lambangState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Lambang Desa</Title>
<TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Judul"
value={lambangState.update.form.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
error={!lambangState.update.form.judul && "Judul wajib diisi"}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={lambangState.update.form.deskripsi}
onChange={(val) => lambangState.update.form.deskripsi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,244 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const maskotState = useProxy(stateProfileDesa.maskotDesa)
const router = useRouter()
const params = useParams()
const [images, setImages] = useState<
Array<{ file: File; preview: string; label: string }>
>([]);
const [formData, setFormData] = useState({
judul: maskotState.update.form.judul || '',
deskripsi: maskotState.update.form.deskripsi || '',
images: [] as Array<{ label: string; imageId: string }>
})
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await maskotState.findUnique.load(id);
if (data) {
// 🔥 INI YANG KURANG!
maskotState.update.initialize(data);
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
images: (data.images || []).map((img: any) => ({
label: img.label,
imageId: img.image?.id ?? '',
})),
});
if (data?.images?.length > 0 && data.images[0].image?.link) {
setImages(data.images.map((img: any) => ({
file: null,
preview: img.image.link,
label: img.label,
})));
}
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
}
};
loadData();
}, [params?.id]);
const handleBack = () => {
router.back()
}
const handleSubmit = async () => {
try {
const uploadedImages = [];
// Upload semua gambar baru
for (const img of images) {
if (!img.file || !(img.file instanceof File)) {
toast.error("File tidak valid untuk di-upload");
continue; // atau return kalau kamu mau hentikan semua
}
const res = await ApiFetch.api.fileStorage.create.post({
file: img.file,
name: img.file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload salah satu gambar");
return;
}
uploadedImages.push({
imageId: uploaded.id,
label: img.label || 'main',
});
}
// Update ke global state
maskotState.update.updateField("judul", formData.judul);
maskotState.update.updateField("deskripsi", formData.deskripsi);
maskotState.update.updateField("images", uploadedImages);
const success = await maskotState.update.submit();
if (success) {
toast.success("Maskot berhasil diperbarui!");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update maskot:", error);
toast.error("Gagal update maskot");
}
};
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '100%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Maskot Desa</Title>
<TextInput
w={{ base: '100%', md: '50%' }}
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Masukkan judul"
value={formData.judul}
onChange={(val) => setFormData({ ...formData, judul: val.currentTarget.value })}
/>
<Box w={{ base: '100%', md: '50%' }}>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box w={{ base: '100%', md: '50%' }}>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,
preview: URL.createObjectURL(file),
label: '',
}));
setImages((prev) => [...prev, ...newImages]);
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
</Box>
</Box>
<SimpleGrid cols={{ base: 2, md: 4 }} >
{images.map((img, index) => (
<Box key={index} mb="md">
<Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}>
<Stack gap={'xs'}>
<Group>
<Button
size="xs"
color="red"
variant="light"
onClick={() => {
const updated = [...images];
updated.splice(index, 1);
setImages(updated);
}}
>
Hapus
</Button>
</Group>
<Image
src={img.preview}
alt={`Preview ${index}`}
width={280}
height={180}
fit="cover"
radius="sm"
/>
<TextInput
label={`Label Gambar ${index + 1}`}
placeholder="Contoh: Logo, Maskot, Dll"
value={img.label}
onChange={(e) => {
const updated = [...images];
updated[index].label = e.currentTarget.value;
setImages(updated);
}}
/>
</Stack>
</Paper>
</Box>
))}
</SimpleGrid>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const sejarahState = useProxy(stateProfileDesa.sejarahDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await sejarahState.findUnique.load(id);
if (data) {
sejarahState.update.initialize(data);
}
};
loadData();
return () => {
sejarahState.update.reset();
sejarahState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !sejarahState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await sejarahState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
sejarahState.findUnique.loading ||
!sejarahState.findUnique.data ||
sejarahState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Sejarah Desa</Title>
<TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Judul"
value={sejarahState.update.form.judul}
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={sejarahState.update.form.deskripsi}
onChange={(val) => sejarahState.update.form.deskripsi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,124 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, 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 Page() {
const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await visiMisiState.findUnique.load(id);
if (data) {
visiMisiState.update.initialize(data);
}
};
loadData();
return () => {
visiMisiState.update.reset();
visiMisiState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
toast.error("Visi wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await visiMisiState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
visiMisiState.findUnique.loading ||
!visiMisiState.findUnique.data ||
visiMisiState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Visi Misi Desa</Title>
<Text fz={"md"} fw={"bold"}>Visi</Text>
<EditEditor
value={visiMisiState.update.form.visi}
onChange={(val) => visiMisiState.update.form.visi = val}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Misi</Text>
<EditEditor
value={visiMisiState.update.form.misi}
onChange={(val) => visiMisiState.update.form.misi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -1,131 +1,168 @@
'use client'
import colors from '@/con/colors';
import { Paper, Stack, Grid, GridCol, Title, Button, Box, Text, Center, Image, SimpleGrid } from '@mantine/core';
import { Box, Button, Card, Center, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { useSnapshot } from 'valtio';
import stateProfileDesa from '../../../_state/desa/profile';
import { useEffect } from 'react';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
function Page() {
const router = useRouter()
const router = useRouter();
const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount
useEffect(() => {
stateProfileDesa.sejarahDesa.findUnique.load("edit");
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
stateProfileDesa.lambangDesa.findUnique.load("edit");
stateProfileDesa.maskotDesa.findUnique.load("edit");
}, []);
const sejarah = snap.sejarahDesa.findUnique.data;
const visiMisi = snap.visiMisiDesa.findUnique.data;
const lambang = snap.lambangDesa.findUnique.data;
const maskot = snap.maskotDesa.findUnique.data;
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Profile Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/profile/edit/sejarah_desa')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"lg"}>
<Title order={2}>Preview Profile Desa</Title>
{/* Sejarah Desa */}
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Sejarah Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
{sejarah && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Sejarah Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>{sejarah.judul}</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
</Paper>
</Paper>
</Paper>
</Stack>
</Box>
</Stack>
</Box>
)}
{/* Visi Misi Desa */}
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Visi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
{visiMisi && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Visi Misi Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Visi Misi Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
</Paper>
</Paper>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Misi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
</Paper>
</Paper>
</Stack>
</Box>
</Stack>
</Box>
)}
{/* Lambang Desa */}
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
{lambang && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Lambang Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
</Paper>
</Paper>
</Paper>
</Stack>
</Box>
</Stack>
</Box>
)}
{/* Maskot Desa */}
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
{maskot && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Maskot Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/pudak-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
<Group wrap="wrap" gap="md">
{maskot.images.map((img, index) => (
<Card key={index} p="xs" w={220}>
<Image
src={img.image.link}
alt={img.label}
w={200}
h={200}
fit="cover"
radius="md"
style={{ border: '1px solid #ccc', objectFit: 'cover' }}
/>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</Card>
))}
</Group>
</Paper>
</Paper>
<SimpleGrid
cols={{
base: 1,
md: 2,
}}
>
<Center>
<Box>
<Paper p={"lg"}>
<Image src={"/api/img/pohonpudak.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Pohon Pudak</Text>
</Paper>
</Box>
</Center>
<Center>
<Box>
<Paper p={"lg"}>
<Image src={"/api/img/bungapudak.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Bunga Pudak</Text>
</Paper>
</Box>
</Center>
</SimpleGrid>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Test
</Text>
</Paper>
</Paper>
</Stack>
</Box>
</Stack>
</Box>
)}
</Stack>
</Paper>
);

View File

@@ -0,0 +1,193 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 ProfilePerbekel() {
const perbekelState = useProxy(stateProfileDesa.profilPerbekel)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-perbekel");
return;
}
const data = await perbekelState.findUnique.load(id);
if (data) {
perbekelState.edit.initialize(data);
}
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
};
loadData();
return () => {
perbekelState.edit.reset();
perbekelState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => {
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
toast.error("Biodata wajib diisi");
return;
}
setIsSubmitting(true)
try {
if (file) {
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = uploadResponse.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
perbekelState.edit.form.imageId = uploaded.id;
}
const success = await perbekelState.edit.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-perbekel");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
perbekelState.findUnique.loading ||
!perbekelState.findUnique.data ||
perbekelState.edit.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box py={10}>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Profil Perbekel</Title>
<Text fz={"md"} fw={"bold"}>Biodata</Text>
<EditEditor
value={perbekelState.edit.form.biodata}
onChange={(val) => perbekelState.edit.form.biodata = val}
/>
{/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Pengalaman</Text>
<EditEditor
value={perbekelState.edit.form.pengalaman}
onChange={(val) => perbekelState.edit.form.pengalaman = val}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Pengalaman Organisasi</Text>
<EditEditor
value={perbekelState.edit.form.pengalamanOrganisasi}
onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Program Unggulan</Text>
<EditEditor
value={perbekelState.edit.form.programUnggulan}
onChange={(val) => perbekelState.edit.form.programUnggulan = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default ProfilePerbekel;

View File

@@ -1,10 +1,110 @@
'use client'
import colors from '@/con/colors';
import { Paper, Text } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useSnapshot } from 'valtio';
import stateProfileDesa from '../../../_state/desa/profile';
function Page() {
const router = useRouter();
const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount
useEffect(() => {
stateProfileDesa.profilPerbekel.findUnique.load("edit");
}, []);
const perbekel = snap.profilPerbekel.findUnique.data;
return (
<Paper bg={colors['white-1']} p={'md'}>
<Text>Test</Text>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Profile PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${snap.profilPerbekel.findUnique.data?.id}`)}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
{perbekel && (
<Box >
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: "md", md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 12 }}>
<Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 12 }}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
</GridCol>
</Grid>
</Box>
<Divider my={"md"} color={colors['blue-button']} />
{/* biodata perbekel */}
<Box px={{ base: 0, md: 50 }} pb={30}>
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 90 }}
src={perbekel.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt='Foto Profil PPID'
onError={(e) => {
e.currentTarget.src = "/perbekel.png";
}}
/>
</Center>
<Paper
bg={colors['blue-button']}
py={20}
className="glass3"
px={{ base: 10, md: 10 }}
>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P.
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box pt={10}>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
</Box>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
</Box>
</Box>
<Box pb={30}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
</Box>
</Box>
<Box pb={20}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
</Box>
</Box>
</Box>
</Paper>
</Box>
)}
</Stack>
</Paper>
);
}

View File

@@ -0,0 +1,140 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: penangananDaruratState.edit.form.name || '',
deskripsi: penangananDaruratState.edit.form.deskripsi || '',
imageId: penangananDaruratState.edit.form.imageId || '',
})
useEffect(() => {
const loadPenangananDarurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await penangananDaruratState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
})
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading penanganan darurat:', error);
toast.error('Gagal memuat data penanganan darurat');
}
}
loadPenangananDarurat();
}, [params?.id])
const handleSubmit = async () => {
try {
penangananDaruratState.edit.form = {
...penangananDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
penangananDaruratState.edit.form.imageId = uploaded.id;
}
await penangananDaruratState.edit.update();
toast.success("Penanganan darurat berhasil diperbarui!");
router.push("/admin/kesehatan/penanganan-darurat");
} catch (error) {
console.error("Error updating penanganan darurat:", error);
toast.error("Gagal memuat data penanganan darurat");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"} w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Penanganan Darurat</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack >
</Box >
);
}
export default EditPenangananDarurat;

View File

@@ -0,0 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { Skeleton } from '@mantine/core';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
penangananDaruratState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
penangananDaruratState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/penanganan-darurat")
}
}
if (!penangananDaruratState.findUnique.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penanganan Darurat</Text>
{penangananDaruratState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Penanganan Darurat</Text>
<Text fz={"lg"}>{penangananDaruratState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: penangananDaruratState.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={penangananDaruratState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (penangananDaruratState.findUnique.data) {
setSelectedId(penangananDaruratState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={penangananDaruratState.delete.loading || !penangananDaruratState.findUnique.data}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${penangananDaruratState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/>
</Box>
);
}
export default DetailPenangananDarurat;

View File

@@ -1,14 +1,56 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function CreatePenangananDarurat() {
const router = useRouter();
const penangananDaruratState = useProxy(penangananDarurat)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
penangananDaruratState.create.form = {
name: "",
deskripsi: "",
imageId: "",
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
penangananDaruratState.create.form.imageId = uploaded.id;
await penangananDaruratState.create.create();
resetForm();
router.push("/admin/kesehatan/penanganan-darurat")
}
return (
<Box>
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -20,54 +62,48 @@ function CreatePenangananDarurat() {
<Title order={3}>Create Penanganan Darurat</Title>
<TextInput
value={penangananDaruratState.create.form.name}
onChange={(val) => {
penangananDaruratState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/> */}
{/* {previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={penangananDaruratState.create.form.deskripsi}
onChange={(val) => {
penangananDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
</Button>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenangananDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penanganan Darurat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Penanganan Darurat</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Penanganan Darurat</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/penanganan-darurat/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailPenangananDarurat;

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button, Image } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditPenangananDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Penanganan Darurat</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Penanganan Darurat</Text>}
placeholder='Masukkan nama Penanganan Darurat'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Penanganan Darurat</Text>}
placeholder='Masukkan deskripsi Penanganan Darurat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Penanganan Darurat</Title>
<Text fw={"bold"} fz={"sm"}>Nama Penanganan Darurat</Text>
<Text fw={"bold"} fz={"sm"}>No Telp Penanganan Darurat</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditPenangananDarurat;

View File

@@ -1,10 +1,14 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function PenangananDarurat() {
return (
@@ -20,7 +24,21 @@ function PenangananDarurat() {
}
function ListPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const router = useRouter();
useShallowEffect(() => {
penangananDaruratState.findMany.load()
}, [])
if (!penangananDaruratState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -34,27 +52,31 @@ function ListPenangananDarurat() {
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
{penangananDaruratState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/penanganan-darurat/detail')}>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>

View File

@@ -0,0 +1,147 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditPosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: statePosyandu.edit.form.name || '',
nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '',
});
useEffect(() => {
const loadPosyandu = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePosyandu.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
loadPosyandu();
}, [params?.id])
const handleSubmit = async () => {
try {
statePosyandu.edit.form = {
...statePosyandu.edit.form,
name: formData.name,
nomor: formData.nomor,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.edit.form.imageId = uploaded.id;
}
await statePosyandu.edit.update();
toast.success("Posyandu berhasil diperbarui!");
router.push("/admin/kesehatan/posyandu");
} catch (error) {
console.error("Error updating posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
/>
<TextInput
value={formData.nomor}
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
statePosyandu.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosyandu;

View File

@@ -0,0 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPosyandu() {
const statePosyandu = useProxy(posyandustate)
const params = useParams()
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePosyandu.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/posyandu")
}
}
if (!statePosyandu.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Posyandu</Text>
{statePosyandu.findUnique.data ? (
<Paper key={statePosyandu.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.nomor}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePosyandu.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button onClick={() => {
if (statePosyandu.findUnique.data) {
setSelectedId(statePosyandu.findUnique.data.id)
setModalHapus(true)
}
}} color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${statePosyandu.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posyandu ini?"
/>
</Box>
);
}
export default DetailPosyandu;

View File

@@ -1,13 +1,59 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
function CreatePosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => {
statePosyandu.create.form = {
name: "",
nomor: "",
deskripsi: "",
imageId: "",
};
setFile(null);
setPreviewImage(null);
}
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.create.form.imageId = uploaded.id;
await statePosyandu.create.create();
resetForm();
router.push("/admin/kesehatan/posyandu")
}
return (
<Box>
<Box mb={10}>
@@ -19,26 +65,52 @@ function CreatePosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Posyandu</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
value={statePosyandu.create.form.name}
onChange={(e) => {
statePosyandu.create.form.name = e.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
value={statePosyandu.create.form.nomor}
onChange={(e) => {
statePosyandu.create.form.nomor = e.target.value;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<KesehatanEditor
showSubmit={false}
<CreateEditor
value={statePosyandu.create.form.deskripsi}
onChange={(htmlContent) => {
statePosyandu.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPosyandu() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Posyandu</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Posyandu</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Posyandu</Text>
<Text fz={"lg"}>089647038426</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/posyandu/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailPosyandu;

View File

@@ -1,49 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
function EditPosyandu() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosyandu;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyandustate from '../../_state/kesehatan/posyandu/posyandu';
import { useShallowEffect } from '@mantine/hooks';
function Posyandu() {
return (
@@ -14,13 +17,27 @@ function Posyandu() {
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPosyandu/>
<ListPosyandu />
</Box>
);
}
function ListPosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findMany.load()
}, [])
if (!statePosyandu.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -28,28 +45,34 @@ function ListPosyandu() {
title='List Posyandu'
href='/admin/kesehatan/posyandu/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posyandu</TableTh>
<TableTh>Nomor Posyandu</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posyandu</TableTh>
<TableTh>Nomor Posyandu</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Posyandu 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Posyandu 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/posyandu/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{statePosyandu.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.nomor}</TableTd>
<TableTd>
<Text fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
);

View File

@@ -0,0 +1,150 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: programKesehatanState.edit.form.name || '',
deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '',
deskripsi: programKesehatanState.edit.form.deskripsi || '',
imageId: programKesehatanState.edit.form.imageId || '',
})
useEffect(() => {
const loadProgramKesehatan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await programKesehatanState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
};
loadProgramKesehatan();
}, [params?.id]);
const handleSubmit = async () => {
try {
programKesehatanState.edit.form = {
...programKesehatanState.edit.form,
name: formData.name,
deskripsiSingkat: formData.deskripsiSingkat,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
programKesehatanState.edit.form.imageId = uploaded.id;
}
await programKesehatanState.edit.update();
toast.success("Program kesehatan berhasil diperbarui!");
router.push("/admin/kesehatan/program-kesehatan");
} catch (error) {
console.error("Error updating program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Program Kesehatan</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack>
</Box >
);
}
export default EditProgramKesehatan;

View File

@@ -0,0 +1,103 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
programKesehatanState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
programKesehatanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/program-kesehatan")
}
}
if (!programKesehatanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={400} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
{programKesehatanState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{programKesehatanState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{programKesehatanState.findUnique.data.deskripsiSingkat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: programKesehatanState.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={programKesehatanState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (programKesehatanState.findUnique.data) {
setSelectedId(programKesehatanState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={programKesehatanState.delete.loading || !programKesehatanState.findUnique.data}
>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${programKesehatanState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kesehatan ini?"
/>
</Box>
);
}
export default DetailProgramKesehatan;

View File

@@ -1,43 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
function CreateProgramKesehatan() {
const router = useRouter();
const programKesehatanState = useProxy(programKesehatan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
// Reset state di valtio
programKesehatanState.create.form = {
name: "",
deskripsiSingkat: "",
deskripsi: "",
imageId: "",
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Simpan ID gambar ke form
programKesehatanState.create.form.imageId = uploaded.id;
// Submit data berita
await programKesehatanState.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/kesehatan/program-kesehatan")
};
return (
<Box>
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Program Kesehatan</Title>
<TextInput
value={programKesehatanState.create.form.name}
onChange={(val) => {
programKesehatanState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
value={programKesehatanState.create.form.deskripsiSingkat}
onChange={(val) => {
programKesehatanState.create.form.deskripsiSingkat = val.target.value;
}}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={programKesehatanState.create.form.deskripsi}
onChange={(val) => {
programKesehatanState.create.form.deskripsi = val;
}}
/>
</Box>
{/* <FileInput
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
@@ -48,25 +106,18 @@ function CreateProgramKesehatan() {
);
setPreviewImage(base64);
}}
/> */}
/>
{/* {previewImage ? (
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
)}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKesehatan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/program-kesehatan/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailProgramKesehatan;

View File

@@ -1,64 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditProgramKesehatan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Kesehatan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kesehatan</Text>}
placeholder='Masukkan nama Program Kesehatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>No Telp Program Kesehatan</Text>}
placeholder='Masukkan no telp Program Kesehatan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Program Kesehatan</Title>
<Text fw={"bold"} fz={"sm"}>Nama Program Kesehatan</Text>
<Text fw={"bold"} fz={"sm"}>No Telp Program Kesehatan</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditProgramKesehatan;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function ProgramKesehatan() {
return (
@@ -14,53 +17,70 @@ function ProgramKesehatan() {
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListProgramKesehatan/>
<ListProgramKesehatan />
</Box>
);
}
function ListProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const router = useRouter()
useShallowEffect(() => {
programKesehatanState.findMany.load()
}, [])
if (!programKesehatanState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Program Kesehatan'
href='/admin/kesehatan/program-kesehatan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/program-kesehatan/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Program Kesehatan'
href='/admin/kesehatan/program-kesehatan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
<TableTh w={250}>Deskripsi Singkat</TableTh>
<TableTh w={250}>Image</TableTh>
<TableTh w={200}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{programKesehatanState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>{item.deskripsiSingkat}</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}

View File

@@ -0,0 +1,286 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { ChangeEvent, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface PuskesmasFormBase {
name: string;
alamat: string;
jam: {
workDays: string;
weekDays: string;
holiday: string;
};
kontak: {
kontakPuskesmas: string;
email: string;
facebook: string;
kontakUGD: string;
};
imageId: string;
}
interface PuskesmasFormData extends PuskesmasFormBase {
image?: {
link: string;
};
}
function EditPuskesmas() {
const statePuskesmas = useProxy(puskesmasState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState<PuskesmasFormData>({
name: statePuskesmas.edit.form.name || '',
alamat: statePuskesmas.edit.form.alamat || '',
jam: {
workDays: statePuskesmas.edit.form.jam?.workDays || '',
weekDays: statePuskesmas.edit.form.jam?.weekDays || '',
holiday: statePuskesmas.edit.form.jam?.holiday || '',
},
kontak: {
kontakPuskesmas: statePuskesmas.edit.form.kontak?.kontakPuskesmas || '',
email: statePuskesmas.edit.form.kontak?.email || '',
facebook: statePuskesmas.edit.form.kontak?.facebook || '',
kontakUGD: statePuskesmas.edit.form.kontak?.kontakUGD || '',
},
imageId: statePuskesmas.edit.form.imageId || '',
});
useEffect(() => {
const loadPuskesmas = async () => {
const id = params?.id as string;
if (!id) return;
try {
await statePuskesmas.edit.load(id);
const { form } = statePuskesmas.edit;
if (form) {
setFormData({
name: form.name,
alamat: form.alamat,
jam: {
workDays: form.jam.workDays,
weekDays: form.jam.weekDays,
holiday: form.jam.holiday,
},
kontak: {
kontakPuskesmas: form.kontak.kontakPuskesmas,
email: form.kontak.email,
facebook: form.kontak.facebook,
kontakUGD: form.kontak.kontakUGD,
},
imageId: form.imageId,
});
// Check if there's an existing image URL in the form data
const formWithImage = form as PuskesmasFormData;
if (formWithImage.image?.link) {
setPreviewImage(formWithImage.image.link);
}
}
} catch (error) {
console.error("Error loading puskesmas:", error);
toast.error("Gagal memuat data puskesmas");
}
};
loadPuskesmas();
}, [params?.id]);
const handleSubmit = async () => {
try {
statePuskesmas.edit.form = {
...statePuskesmas.edit.form,
name: formData.name,
alamat: formData.alamat,
jam: {
workDays: formData.jam.workDays,
weekDays: formData.jam.weekDays,
holiday: formData.jam.holiday,
},
kontak: {
kontakPuskesmas: formData.kontak.kontakPuskesmas,
email: formData.kontak.email,
facebook: formData.kontak.facebook,
kontakUGD: formData.kontak.kontakUGD,
},
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
statePuskesmas.edit.form.imageId = uploaded.id;
}
const success = await statePuskesmas.edit.submit();
if (success) {
toast.success("Puskesmas berhasil diperbarui!");
router.push("/admin/kesehatan/puskesmas");
}
} catch (error) {
console.error("Error updating puskesmas:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas");
}
};
const handleFileChange = (selectedFile: File | null) => {
if (selectedFile) {
setFile(selectedFile);
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
setPreviewImage(e.target.result as string);
}
};
reader.readAsDataURL(selectedFile);
} else {
setFile(null);
setPreviewImage(null);
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleNestedChange = (section: 'jam' | 'kontak', field: string, value: string) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}));
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant="subtle" color="blue">
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap="xs">
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Puskesmas</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>}
placeholder="masukkan nama puskesmas"
name="name"
value={formData.name}
onChange={handleInputChange}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
name="alamat"
value={formData.alamat}
onChange={handleInputChange}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>}
placeholder="masukkan jam buka"
value={formData.jam.workDays}
onChange={(e) => handleNestedChange('jam', 'workDays', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>}
placeholder="masukkan jam tutup"
value={formData.jam.weekDays}
onChange={(e) => handleNestedChange('jam', 'weekDays', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Libur</Text>}
placeholder="masukkan jam libur"
value={formData.jam.holiday}
onChange={(e) => handleNestedChange('jam', 'holiday', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>}
placeholder="masukkan kontak puskesmas"
value={formData.kontak.kontakPuskesmas}
onChange={(e) => handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Email</Text>}
placeholder="masukkan email"
value={formData.kontak.email}
onChange={(e) => handleNestedChange('kontak', 'email', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>}
placeholder="masukkan facebook"
value={formData.kontak.facebook}
onChange={(e) => handleNestedChange('kontak', 'facebook', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak UGD"
value={formData.kontak.kontakUGD}
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/>
<FileInput
placeholder="Pilih gambar"
label="Gambar"
accept="image/*"
leftSection={<IconImageInPicture size={16} />}
value={file}
onChange={handleFileChange}
/>
{previewImage ? (
<Image alt="Preview" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
loading={statePuskesmas.edit.loading}
>
Simpan Perubahan
</Button>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditPuskesmas;

View File

@@ -0,0 +1,111 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPuskesmas() {
const params = useParams()
const router = useRouter();
const statePuskesmas = useProxy(puskesmasState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
statePuskesmas.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePuskesmas.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/puskesmas")
}
}
if (!statePuskesmas.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Puskesmas</Text>
{statePuskesmas.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Puskesmas</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jam Operasional</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.workDays}</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.weekDays}</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.holiday}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePuskesmas.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kontak</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakPuskesmas}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.email}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.facebook}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakUGD}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (statePuskesmas.findUnique.data) {
setSelectedId(statePuskesmas.findUnique.data.id)
setModalHapus(true)
}
}}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${statePuskesmas.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/>
</Box>
);
}
export default DetailPuskesmas;

View File

@@ -1,14 +1,76 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
function CreatePuskesmas() {
const statePuskesmas = useProxy(puskesmasState)
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => {
statePuskesmas.create.form = {
name: "",
alamat: "",
jam: {
workDays: "",
weekDays: "",
holiday: "",
},
kontak: {
kontakPuskesmas: "",
email: "",
facebook: "",
kontakUGD: "",
},
imageId: "",
image: undefined,
};
setFile(null);
setPreviewImage(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePuskesmas.create.form.imageId = uploaded.id;
// State is already being updated directly in the form inputs
await statePuskesmas.create.submit();
toast.success("Data berhasil disimpan");
resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/puskesmas');
}
return (
<Box>
<Box component="form" onSubmit={handleSubmit}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -18,26 +80,80 @@ function CreatePuskesmas() {
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Puskesmas</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>}
placeholder="masukkan nama puskesmas"
value={statePuskesmas.create.form.name}
onChange={(e) => {
statePuskesmas.create.form.name = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
value={statePuskesmas.create.form.alamat}
onChange={(e) => {
statePuskesmas.create.form.alamat = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>}
placeholder="masukkan jam buka"
value={statePuskesmas.create.form.jam.workDays}
onChange={(e) => {
statePuskesmas.create.form.jam.workDays = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>}
placeholder="masukkan jam tutup"
value={statePuskesmas.create.form.jam.weekDays}
onChange={(e) => {
statePuskesmas.create.form.jam.weekDays = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Holiday</Text>}
placeholder="masukkan holiday"
value={statePuskesmas.create.form.jam.holiday}
onChange={(e) => {
statePuskesmas.create.form.jam.holiday = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>}
placeholder="masukkan kontak puskesmas"
value={statePuskesmas.create.form.kontak.kontakPuskesmas}
onChange={(e) => {
statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Email</Text>}
placeholder="masukkan email"
value={statePuskesmas.create.form.kontak.email}
onChange={(e) => {
statePuskesmas.create.form.kontak.email = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>}
placeholder="masukkan facebook"
value={statePuskesmas.create.form.kontak.facebook}
onChange={(e) => {
statePuskesmas.create.form.kontak.facebook = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak ugd"
value={statePuskesmas.create.form.kontak.kontakUGD}
onChange={(e) => {
statePuskesmas.create.form.kontak.kontakUGD = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
{/* <FileInput
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
@@ -48,25 +164,18 @@ function CreatePuskesmas() {
);
setPreviewImage(base64);
}}
/> */}
/>
{/* {previewImage ? (
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
)}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Puskesmas
</Button>
</Stack>
</Paper>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPuskesmas() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/puskesmas/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPuskesmas;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import puskesmasState from '../../_state/kesehatan/puskesmas/puskesmas';
function Puskesmas() {
return (
@@ -20,7 +23,20 @@ function Puskesmas() {
}
function ListPuskesmas() {
const statePuskesmas = useProxy(puskesmasState)
const router = useRouter();
useShallowEffect(() => {
statePuskesmas.findMany.load()
}, [])
if (!statePuskesmas.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500}/>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -33,28 +49,27 @@ function ListPuskesmas() {
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/puskesmas/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
{statePuskesmas.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Image w={100} src={item.image.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>

View File

@@ -12,7 +12,7 @@ function Page() {
const router = useRouter()
const allList = useProxy(stateProfilePPID)
useShallowEffect(() => {
allList.profile.load("1") // Assuming "1" is your default ID, adjust as needed
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed
}, [])
if (!allList.profile.data) {

View File

@@ -42,6 +42,7 @@ function VisiMisiPPIDEdit() {
visiMisi.findById.data.misi = draftMisi;
visiMisi.update.save(visiMisi.findById.data);
}
router.push('/admin/ppid/visi-misi-ppid')
};
return (

View File

@@ -51,7 +51,7 @@ function VisiMisiPPIDList() {
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/api/img/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} fw={"bold"}>
MOTO PPID DESA DARMASABA

View File

@@ -11,41 +11,26 @@ export const navBar = [
},
{
id: "Landing_Page_2",
name: "Penghargaan",
path: "/admin/landing-page/penghargaan"
},
{
id: "Landing_Page_3",
name: "Layanan",
path: "/admin/landing-page/layanan"
},
{
id: "Landing_Page_4",
name: "Potensi",
path: "/admin/landing-page/potensi"
},
{
id: "Landing_Page_5",
name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi"
},
{
id: "Landing_Page_6",
id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat"
},
{
id: "Landing_Page_7",
id: "Landing_Page_4",
name: "SDGs Desa",
path: "/admin/landing-page/sdgs-desa"
},
{
id: "Landing_Page_8",
id: "Landing_Page_5",
name: "APBDes",
path: "/admin/landing-page/apbdes"
},
{
id: "Landing_Page_9",
id: "Landing_Page_6",
name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa"
}

View File

@@ -3,6 +3,7 @@ import VisiMisiDesa from "./visi-misi";
import LambangDesa from "./lambang-desa";
import MaskotDesa from "./maskot-desa";
import Elysia from "elysia";
import ProfilPerbekel from "../profilePerbekel";
const ProfileDesa = new Elysia({
prefix: "/profile",
@@ -11,6 +12,7 @@ const ProfileDesa = new Elysia({
.use(SejarahDesa)
.use(VisiMisiDesa)
.use(LambangDesa)
.use(MaskotDesa);
.use(MaskotDesa)
.use(ProfilPerbekel);
export default ProfileDesa;

View File

@@ -3,10 +3,10 @@ import maskotDesaFindById from "./find-by-id";
import Elysia, { t } from "elysia";
const MaskotDesa = new Elysia({
prefix: "/maskot",
tags: ["Desa/Profile"],
prefix: "/maskot",
tags: ["Desa/Profile"],
})
.get("/:id", async (context) => {
.get("/:id", async (context) => {
const response = await maskotDesaFindById(new Request(context.request));
return response;
})
@@ -18,7 +18,8 @@ const MaskotDesa = new Elysia({
},
{
body: t.Object({
maskot: t.String(),
judul: t.String(),
deskripsi: t.String(),
images: t.Array(
t.Object({
imageId: t.String(),
@@ -27,5 +28,5 @@ const MaskotDesa = new Elysia({
),
}),
}
)
export default MaskotDesa;
);
export default MaskotDesa;

View File

@@ -20,6 +20,11 @@ import FirstAid from "./data_kesehatan_warga/artikel_kesehatan/first_aid";
import MythVsFact from "./data_kesehatan_warga/artikel_kesehatan/myth_vs_fact";
import DoctorSign from "./data_kesehatan_warga/artikel_kesehatan/doctor_sign";
import Posyandu from "./posyandu";
import Puskesmas from "./puskesmas";
import ProgramKesehatan from "./program-kesehatan";
import PenangananDarurat from "./penanganan-darurat";
import KontakDarurat from "./kontak-darurat";
import InfoWabahPenyakit from "./info-wabah-penyakit";
const Kesehatan = new Elysia({
@@ -47,4 +52,9 @@ const Kesehatan = new Elysia({
.use(MythVsFact)
.use(DoctorSign)
.use(Posyandu)
.use(Puskesmas)
.use(ProgramKesehatan)
.use(PenangananDarurat)
.use(KontakDarurat)
.use(InfoWabahPenyakit)
export default Kesehatan;

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.InfoWabahPenyakitGetPayload<{
select: {
name: true;
deskripsiSingkat: true;
deskripsiLengkap: true;
imageId: true;
};
}>
export default async function infoWabahPenyakitCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.infoWabahPenyakit.create({
data: {
name: body.name,
deskripsiSingkat: body.deskripsiSingkat,
deskripsiLengkap: body.deskripsiLengkap,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create info wabah penyakit",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import fs from "fs/promises";
import { Context } from "elysia";
import path from "path";
const infoWabahPenyakitDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const infoWabahPenyakit = await prisma.infoWabahPenyakit.findUnique({
where: { id },
include: { image: true },
});
if (!infoWabahPenyakit) {
return {
status: 404,
body: "Info wabah penyakit tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (infoWabahPenyakit.image) {
try {
const filePath = path.join(infoWabahPenyakit.image.path, infoWabahPenyakit.image.name);
await fs.unlink(filePath);
} catch (err) {
console.error("Gagal hapus gambar:", err);
}
}
await prisma.infoWabahPenyakit.delete({
where: { id },
});
return {
status: 200,
body: "Info wabah penyakit berhasil dihapus",
};
};
export default infoWabahPenyakitDelete;

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
export default async function infoWabahPenyakitFindMany() {
try {
const data = await prisma.infoWabahPenyakit.findMany({
where: {
isActive: true,
},
include: {
image: true,
}
})
return {
success: true,
message: "Success fetch info wabah penyakit",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch info wabah penyakit",
}
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function infoWabahPenyakitFindUnique(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.infoWabahPenyakit.findUnique({
where: {id},
include: {
image: true
}
})
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 })
}
return Response.json({
success: true,
message: "Success fetch info wabah penyakit by ID",
data,
}, { status: 200 })
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil info wabah penyakit: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 })
}
}

View File

@@ -0,0 +1,40 @@
import Elysia, { t } from "elysia";
import infoWabahPenyakitCreate from "./create";
import infoWabahPenyakitFindMany from "./find-many";
import infoWabahPenyakitDelete from "./del";
import infoWabahPenyakitFindUnique from "./findUnique";
import infoWabahPenyakitUpdate from "./updt";
const InfoWabahPenyakit = new Elysia({
prefix: "/infowabahpenyakit",
tags: ["Kesehatan/Info Wabah Penyakit"]
})
.post("/create", infoWabahPenyakitCreate, {
body: t.Object({
name: t.String(),
deskripsiSingkat: t.String(),
deskripsiLengkap: t.String(),
imageId: t.String(),
})
})
.get("/find-many", infoWabahPenyakitFindMany)
.delete("/del/:id", infoWabahPenyakitDelete)
.get("/:id", async (context) => {
const response = await infoWabahPenyakitFindUnique(new Request(context.request));
return response;
})
.put("/:id", async (context) => {
const response = await infoWabahPenyakitUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsiSingkat: t.String(),
deskripsiLengkap: t.String(),
imageId: t.String(),
})
}
)
export default InfoWabahPenyakit;

View File

@@ -0,0 +1,105 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.InfoWabahPenyakitGetPayload<{
select: {
id: true;
name: true;
deskripsiSingkat: true;
deskripsiLengkap: true;
imageId: true;
}
}>
export default async function infoWabahPenyakitUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
deskripsiSingkat,
deskripsiLengkap,
imageId,
} = body;
if(!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.infoWabahPenyakit.findUnique({
where: { id },
include: {
image: true,
}
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Info wabah penyakit tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.infoWabahPenyakit.update({
where: { id },
data: {
name,
deskripsiSingkat,
deskripsiLengkap,
imageId,
}
})
return new Response(JSON.stringify({
success: true,
message: "Info wabah penyakit berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating info wabah penyakit:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate info wabah penyakit",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.KontakDaruratGetPayload<{
select: {
name: true;
deskripsi: true;
imageId: true;
};
}>;
export default async function kontakDaruratCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.kontakDarurat.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create kontak darurat",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import fs from "fs/promises";
import { Context } from "elysia";
import path from "path";
const kontakDaruratDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const kontakDarurat = await prisma.kontakDarurat.findUnique({
where: { id },
include: { image: true },
});
if (!kontakDarurat) {
return {
status: 404,
body: "Kontak darurat tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (kontakDarurat.image) {
try {
const filePath = path.join(kontakDarurat.image.path, kontakDarurat.image.name);
await fs.unlink(filePath);
} catch (error) {
console.error("Error deleting image file:", error);
}
}
await prisma.kontakDarurat.delete({
where: { id },
});
return {
status: 200,
body: "Kontak darurat berhasil dihapus",
};
};
export default kontakDaruratDelete;

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
export default async function kontakDaruratFindMany() {
try {
const data = await prisma.kontakDarurat.findMany({
where: {
isActive: true,
},
include: {
image: true,
}
})
return {
success: true,
message: "Success fetch kontak darurat",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch kontak darurat",
}
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function kontakDaruratFindUnique(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.kontakDarurat.findUnique({
where: {id},
include: {
image: true
}
})
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 })
}
return Response.json({
success: true,
message: "Success fetch kontak darurat",
data,
}, { status: 200 })
} catch (error) {
console.error("Find unique error:", error);
return Response.json({
success: false,
message: "Failed fetch kontak darurat",
}, { status: 500 })
}
}

View File

@@ -0,0 +1,42 @@
import Elysia, { t } from "elysia";
import kontakDaruratCreate from "./create";
import kontakDaruratFindMany from "./find-many";
import kontakDaruratDelete from "./del";
import kontakDaruratUpdate from "./updt";
import kontakDaruratFindUnique from "./findUnique";
const KontakDarurat = new Elysia({
prefix: "/kontakdarurat",
tags: ["Kesehatan/Kontak Darurat"]
})
.post("/create", kontakDaruratCreate, {
body: t.Object({
name: t.String(),
nomor: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
})
.get("/find-many", kontakDaruratFindMany)
.delete("/del/:id", kontakDaruratDelete)
.get("/:id", async (context) => {
const response = await kontakDaruratFindUnique(new Request(context.request));
return response;
})
.put(
"/:id",
async (context) => {
const response = await kontakDaruratUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
nomor: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
}
)
export default KontakDarurat;

View File

@@ -0,0 +1,102 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.KontakDaruratGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
imageId: true;
}
}>
export default async function kontakDaruratUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
deskripsi,
imageId,
} = body;
if(!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.kontakDarurat.findUnique({
where: { id },
include: {
image: true,
}
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Kontak Darurat tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.kontakDarurat.update({
where: { id },
data: {
name,
deskripsi,
imageId,
}
})
return new Response(JSON.stringify({
success: true,
message: "Kontak Darurat berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating kontak darurat:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate kontak darurat",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PenangananDaruratGetPayload<{
select: {
name: true;
deskripsi: true;
imageId: true;
};
}>;
export default async function penangananDaruratCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.penangananDarurat.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create penanganan darurat",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,54 @@
import prisma from "@/lib/prisma";
import fs from "fs/promises";
import { Context } from "elysia";
import path from "path";
const penangananDaruratDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const penangananDarurat = await prisma.penangananDarurat.findUnique({
where: { id },
include: { image: true },
});
if (!penangananDarurat) {
return {
status: 404,
body: "Penanganan darurat tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (penangananDarurat.image) {
try {
const filePath = path.join(
penangananDarurat.image.path,
penangananDarurat.image.name
);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: penangananDarurat.image.id },
});
} catch (error) {
console.error("Error deleting image file:", error);
}
}
// Hapus data dari database
await prisma.penangananDarurat.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Penanganan darurat dan file terkait berhasil dihapus",
};
};
export default penangananDaruratDelete;

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
export default async function penangananDaruratFindMany() {
try {
const data = await prisma.penangananDarurat.findMany({
where: {
isActive: true,
},
include: {
image: true,
}
})
return {
success: true,
message: "Success fetch penanganan darurat",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch penanganan darurat",
}
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function penangananDaruratFindUnique(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.penangananDarurat.findUnique({
where: {id},
include: {
image: true
}
})
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 })
}
return Response.json({
success: true,
message: "Success fetch penanganan darurat by ID",
data,
}, { status: 200 })
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil penanganan darurat: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 })
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import penangananDaruratCreate from "./create";
import penangananDaruratDelete from "./del";
import penangananDaruratFindMany from "./find-many";
import penangananDaruratFindUnique from "./findUnique";
import penangananDaruratUpdate from "./updt";
const PenangananDarurat = new Elysia({
prefix: "/penanganandarurat",
tags: ["Kesehatan/Penanganan Darurat"]
})
.post("/create", penangananDaruratCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
})
.get("/find-many", penangananDaruratFindMany)
.delete("/del/:id", penangananDaruratDelete)
.get(":id", async (context) => {
const response = await penangananDaruratFindUnique(new Request(context.request));
return response;
})
.put(
":id",
async (context) => {
const response = await penangananDaruratUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
}
)
export default PenangananDarurat;

View File

@@ -0,0 +1,103 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.PenangananDaruratGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
imageId: true;
}
}>
export default async function penangananDaruratUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
deskripsi,
imageId,
} = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.penangananDarurat.findUnique({
where: { id },
include: {
image: true,
}
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Penanganan darurat tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.penangananDarurat.update({
where: { id },
data: {
name,
deskripsi,
imageId,
}
})
return new Response(JSON.stringify({
success: true,
message: "Penanganan darurat berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating penanganan darurat:", error);
return new Response(JSON.stringify({
success: false,
message: "Gagal mengupdate penanganan darurat: " + (error instanceof Error ? error.message : 'Unknown error'),
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
})
}
}

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.ProgramKesehatanGetPayload<{
select: {
name: true;
deskripsiSingkat: true;
deskripsi: true;
imageId: true;
}
}>
export default async function programKesehatanCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.programKesehatan.create({
data: {
name: body.name,
deskripsiSingkat: body.deskripsiSingkat,
deskripsi: body.deskripsi,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create program kesehatan",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,52 @@
import prisma from "@/lib/prisma";
import fs from "fs/promises";
import { Context } from "elysia";
import path from "path";
const programKesehatanDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const programKesehatan = await prisma.programKesehatan.findUnique({
where: { id },
include: { image: true },
});
if (!programKesehatan) {
return {
status: 404,
body: "Program kesehatan tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (programKesehatan.image) {
try {
const filePath = path.join(programKesehatan.image.path, programKesehatan.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: programKesehatan.image.id },
});
} catch (error) {
console.error("Gagal hapus file image:", error);
}
}
// Hapus berita dari DB
await prisma.programKesehatan.delete({
where: { id },
});
return {
success: true,
message: "Program kesehatan dan file terkait berhasil dihapus",
};
};
export default programKesehatanDelete;

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
export default async function programKesehatanFindMany() {
try {
const data = await prisma.programKesehatan.findMany({
where: {
isActive: true,
},
include: {
image: true,
}
})
return {
success: true,
message: "Success fetch program kesehatan",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch program kesehatan",
}
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function programKesehatanFindUnique(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.programKesehatan.findUnique({
where: {id},
include: {
image: true
}
})
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 })
}
return Response.json({
success: true,
message: "Success fetch program kesehatan by ID",
data,
}, { status: 200 })
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil program kesehatan: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 })
}
}

View File

@@ -0,0 +1,41 @@
import Elysia, { t } from "elysia";
import programKesehatanCreate from "./create";
import programKesehatanFindMany from "./find-many";
import programKesehatanDelete from "./del";
import programKesehatanFindUnique from "./findUnique";
import programKesehatanUpdate from "./updt";
const ProgramKesehatan = new Elysia({
prefix: "/programkesehatan",
tags: ["Kesehatan/Program Kesehatan"],
})
.post("/create", programKesehatanCreate, {
body: t.Object({
name: t.String(),
deskripsiSingkat: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
})
.get("/find-many", programKesehatanFindMany)
.delete("/del/:id", programKesehatanDelete)
.get("/:id", async (context) => {
const response = await programKesehatanFindUnique(new Request(context.request));
return response;
})
.put(
"/:id",
async (context) => {
const response = await programKesehatanUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsiSingkat: t.String(),
deskripsi: t.String(),
imageId: t.String(),
})
}
)
export default ProgramKesehatan;

View File

@@ -0,0 +1,115 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.ProgramKesehatanGetPayload<{
select: {
id: true;
name: true;
deskripsiSingkat: true;
deskripsi: true;
imageId: true;
};
}>;
export default async function programKesehatanUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsiSingkat, deskripsi, imageId } = body;
if (!id) {
return new Response(
JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
const existing = await prisma.programKesehatan.findUnique({
where: { id },
include: {
image: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Program kesehatan tidak ditemukan",
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
);
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.programKesehatan.update({
where: { id },
data: {
name,
deskripsiSingkat,
deskripsi,
imageId,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Success update program kesehatan",
data: updated,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
} catch (error) {
console.error("Update error:", error);
return new Response(
JSON.stringify({
success: false,
message:
"Gagal mengupdate program kesehatan: " +
(error instanceof Error ? error.message : "Unknown error"),
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
}

View File

@@ -0,0 +1,69 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type JamOperasionalInput = {
workDays: string;
weekDays: string;
holiday: string;
};
type KontakPuskesmasInput = {
kontakPuskesmas: string;
email: string;
facebook: string;
kontakUGD: string;
};
type CreatePuskesmasInput = {
name: string;
alamat: string;
imageId: string;
jam: JamOperasionalInput;
kontak: KontakPuskesmasInput;
};
const puskesmasCreate = async (context: Context) => {
const { name, alamat, imageId, jam, kontak } = await context.body as CreatePuskesmasInput;
// 1. Buat jam operasional
const createdJam = await prisma.jamOperasional.create({
data: {
workDays: jam.workDays,
weekDays: jam.weekDays,
holiday: jam.holiday
}
});
// 2. Buat kontak puskesmas
const createdKontak = await prisma.kontakPuskesmas.create({
data: {
kontakPuskesmas: kontak.kontakPuskesmas,
email: kontak.email,
facebook: kontak.facebook,
kontakUGD: kontak.kontakUGD
}
});
// 3. Buat puskesmas
const createdPuskesmas = await prisma.puskesmas.create({
data: {
name,
alamat,
imageId,
jamId: createdJam.id,
kontakId: createdKontak.id
},
include: {
image: true,
jam: true,
kontak: true
}
});
return {
success: true,
data: createdPuskesmas
};
};
export default puskesmasCreate;

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
const puskesmasDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const puskesmas = await prisma.puskesmas.findUnique({
where: { id },
include: { image: true, kontak: true, jam: true },
});
if (!puskesmas) {
return {
status: 404,
body: "Puskesmas tidak ditemukan",
};
}
if (puskesmas.image) {
try {
const filePath = path.join(puskesmas.image.path, puskesmas.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: puskesmas.image.id },
});
} catch (error) {
console.error("Gagal hapus file image:", error);
}
}
await prisma.puskesmas.delete({
where: { id },
})
return {
success: true,
message: "Puskesmas dan file terkait berhasil dihapus",
}
};
export default puskesmasDelete;

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
export default async function puskesmasFindMany() {
try {
const data = await prisma.puskesmas.findMany({
where: {
isActive: true,
},
include: {
image: true,
jam: true,
kontak: true,
}
})
return {
success: true,
message: "Success fetch puskesmas",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch puskesmas",
}
}
}

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
export default async function findUniquePuskesmas(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.puskesmas.findUnique({
where: {id},
include: {
image: true,
jam: true,
kontak: true,
}
})
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 })
}
return Response.json({
success: true,
message: "Success fetch puskesmas by ID",
data,
}, { status: 200 })
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil puskesmas: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 })
}
}

View File

@@ -0,0 +1,62 @@
import Elysia, { t } from "elysia";
import puskesmasCreate from "./create";
import puskesmasDelete from "./del";
import puskesmasFindMany from "./find-many";
import findUniquePuskesmas from "./findUnique";
import puskesmasUpdate from "./updt";
const Puskesmas = new Elysia({
prefix: "puskesmas",
tags: ["Kesehatan/Puskesmas"],
})
.post("/create", puskesmasCreate, {
body: t.Object({
name: t.String(),
alamat: t.String(),
imageId: t.String(),
jam: t.Object({
workDays: t.String(),
weekDays: t.String(),
holiday: t.String(),
}),
kontak: t.Object({
kontakPuskesmas: t.String(),
email: t.String(),
facebook: t.String(),
kontakUGD: t.String(),
}),
}),
})
.get("/find-many", puskesmasFindMany)
.delete("/del/:id", puskesmasDelete)
.get("/:id", async (context) => {
const response = await findUniquePuskesmas(new Request(context.request));
return response;
})
.put(
"/:id",
async (context) => {
const response = await puskesmasUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
alamat: t.String(),
imageId: t.String(),
jam: t.Object({
workDays: t.String(),
weekDays: t.String(),
holiday: t.String(),
}),
kontak: t.Object({
kontakPuskesmas: t.String(),
email: t.String(),
facebook: t.String(),
kontakUGD: t.String(),
}),
}),
}
);
export default Puskesmas;

View File

@@ -0,0 +1,139 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormUpdate = Prisma.PuskesmasGetPayload<{
select: {
id: true;
name: true;
alamat: true;
jam: true;
imageId: true;
kontak: true;
};
}>;
export default async function puskesmasUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, alamat, jam, imageId, kontak } = body;
if (!id) {
return new Response(
JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
const existing = await prisma.puskesmas.findUnique({
where: { id },
include: {
image: true,
kontak: true,
jam: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Puskesmas tidak ditemukan",
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
);
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
await prisma.jamOperasional.update({
where: { id: existing.jamId },
data: {
workDays: jam.workDays,
weekDays: jam.weekDays,
holiday: jam.holiday,
},
});
await prisma.kontakPuskesmas.update({
where: { id: existing.kontakId },
data: {
kontakPuskesmas: kontak.kontakPuskesmas,
email: kontak.email,
facebook: kontak.facebook,
kontakUGD: kontak.kontakUGD,
},
});
const updated = await prisma.puskesmas.update({
where: { id },
data: {
name,
alamat,
jamId: jam.id,
imageId,
kontakId: kontak.id,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Success update puskesmas",
data: updated,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
} catch (error) {
console.error("Update error:", error);
return new Response(
JSON.stringify({
success: false,
message:
"Gagal update puskesmas: " +
(error instanceof Error ? error.message : "Unknown error"),
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
}

View File

@@ -11,6 +11,7 @@ import '@mantine/charts/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/tiptap/styles.css';
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
import {
ColorSchemeScript,