Compare commits

..

18 Commits

Author SHA1 Message Date
ed371bd0d9 Fix QC Kak Inno 24 Okt 25
Fix QC Kak Ayu 24 Okt 25
Fix QC Keano 24 Okt 25
Fix Detail Lowongan Kerja
2025-10-27 22:15:55 +08:00
f82c7b86e0 27 Oct 2025-10-27 10:54:50 +08:00
b5d6585cd5 27 Oct 2025-10-27 10:54:01 +08:00
aa98359ef7 Fix Revisi Kak Inno 22 Oktober && Fix Revisi Kak Ayu 22 Oktober 2025-10-23 17:45:45 +08:00
0ff0d5234a Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober 2025-10-22 17:00:12 +08:00
827c1c191a Revisi QC Kak Inno tanggal 20 2025-10-22 09:58:16 +08:00
fb596f9033 Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25 2025-10-21 12:17:30 +08:00
9055b40769 Fix navbar mobile add active page 2025-10-19 18:08:49 +08:00
bbf13c1cf7 Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
2025-10-17 17:45:56 +08:00
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
ccf39bc778 Penambahan fungsi search disetiap menu & submenu,
Menu Landing Page
Menu PPID
Menu Desa
2025-10-15 10:13:02 +08:00
3c21f7742c Yang sudh dikerjakan:
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya ( status buku bisa engga otomatis dipinjemnya), kalau dikembalikan statusnya otomatis
)
Yang Mau Dikerjakan:
Cek fungsi menu yang kompleks
2025-10-14 10:38:55 +08:00
a158241c0b - QC User & Admin Menu Pendidikan V
- Fix SubMenu :
- Beasiswa Desa ( Baca Selengkapnya terdapatkan konten ) V
- Info Sekolah ( Kategori Menyesuaikan Dengan Datanya ) V
- Perpustakaan Digital (  V
- Kategori Menyesuaikan Dengan Datanya V
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya V
)
2025-10-13 11:20:38 +08:00
80c5dc6361 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 17:06:21 +08:00
8ad38fc907 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 14:02:11 +08:00
d601b2fee3 Fix Bug SubMenu Struktur PPID, SubMenu Struktur Organisasi BumDes 2025-10-07 14:38:20 +08:00
cee0957e07 QC - User & Admin Menu Ekonomi SubMenu Pasar Desa
Fix bug kategori produk
2025-10-06 10:26:59 +08:00
221 changed files with 11846 additions and 4913 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0",
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
@@ -43,6 +43,7 @@
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
@@ -52,6 +53,7 @@
"classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
@@ -71,6 +73,7 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
@@ -82,6 +85,7 @@
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

View File

@@ -1,5 +1,4 @@
[
{ "name": "Semua" },
{ "name": "Pemerintahan" },
{ "name": "Pembangunan" },
{ "name": "Ekonomi" },

View File

@@ -1,8 +0,0 @@
[
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"atasanId": "550e8400-e29b-41d4-a716-446655440001",
"bawahanId": "550e8400-e29b-41d4-a716-446655440002",
"tipe": "Langsung Melapor"
}
]

View File

@@ -0,0 +1,91 @@
[
{
"id": "cmgewz4gt000704ib91i3f169",
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
"gelarAkademik": "S.H.,M.H.,NL.P.",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
"email": "bagus@desa.id",
"telepon": "081234567891",
"alamat": "Jl. Raya Desa No. 1",
"posisiId": "kepala_desa",
"isActive": true
},
{
"id": "cmgewxfvw000004ibee5013f4",
"namaLengkap": "I Ketut Suwanta",
"gelarAkademik": "S.Pt",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "suwanta@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "sekretaris_desa",
"isActive": true
},
{
"id": "cmgewxvqw000104ibgm5l8fzs",
"namaLengkap": "Ni Wayan Supardiati",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "supardiati@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kaur_keuangan",
"isActive": true
},
{
"id": "cmgewy1g9000204ib2n7hbx0i",
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "agus@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_menesa",
"isActive": true
},
{
"id": "cmgewybah000304ibgqhn1gm2",
"namaLengkap": "I Wayan Sueca",
"gelarAkademik": "S.H.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "sueca@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_darmasaba",
"isActive": true
},
{
"id": "cmgewygqz000404ib20sv8nvg",
"namaLengkap": "Si Gede Ketut Astawa",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "astawa@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_bucu",
"isActive": true
},
{
"id": "cmgewyos1000504ibcu8o2gyk",
"namaLengkap": "I Kadek Arya Minarta",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "minarta@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_gulingan",
"isActive": true
},
{
"id": "cmgewyxk7000604ib8djs3i6c",
"namaLengkap": "I Gede Andika Pradnya Diputra",
"gelarAkademik": "S.E.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "diputra@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_taman",
"isActive": true
}
]

View File

@@ -1,24 +0,0 @@
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"namaLengkap": "Budi Santoso",
"gelarAkademik": "S.IP",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
"email": "budi@desa.id",
"telepon": "081234567891",
"alamat": "Jl. Raya Desa No. 1",
"posisiId": "kepala_desa",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"namaLengkap": "Ani Lestari",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "ani@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "sekretaris_desa",
"isActive": true
}
]

View File

@@ -0,0 +1,159 @@
[
[
{
"id": "kepala_desa",
"nama": "Kepala Desa",
"deskripsi": "Pemimpin desa Darmasaba",
"hierarki": 1,
"parentId": null
},
{
"id": "kepala_urusan",
"nama": "Kepala Urusan",
"deskripsi": "Pemimpin urusan desa Darmasaba",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "sekretaris_desa",
"nama": "Sekretaris Desa",
"deskripsi": "Pengelola administrasi desa",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kaur_keuangan",
"nama": "Kaur Keuangan",
"deskripsi": "Pengelola keuangan desa",
"hierarki": 3,
"parentId": "kaur_umum"
},
{
"id": "kaur_perencanaan",
"nama": "Kaur Perencanaan",
"deskripsi": "Penyusun program kerja desa",
"hierarki": 3,
"parentId": "kaur_umum"
},
{
"id": "kaur_umum",
"nama": "Kaur Umum & TU",
"deskripsi": "Pelayanan umum dan administrasi",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_pemerintahan",
"nama": "Kasi Pemerintahan",
"deskripsi": "Urusan pemerintahan dan keamanan",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_pelayanan",
"nama": "Kasi Pelayanan",
"deskripsi": "Urusan pelayanan masyarakat",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_kesejahteraan",
"nama": "Kasi Kesejahteraan",
"deskripsi": "Urusan sosial dan kesejahteraan",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kadus_banjar_dinas_cabe",
"nama": "Kepala Dusun Banjar Dinas Cabe",
"deskripsi": "Pimpinan wilayah Banjar Dinas Cabe",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_menesa",
"nama": "Kepala Dusun Banjar Dinas Menesa",
"deskripsi": "Pimpinan wilayah Banjar Menesa",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_penenjoan",
"nama": "Kepala Dusun Banjar Dinas Penenjoan",
"deskripsi": "Pimpinan wilayah Banjar Dinas Penenjoan",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_telanga",
"nama": "Kepala Dusun Banjar Dinas Telanga",
"deskripsi": "Pimpinan wilayah Banjar Dinas Telanga",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_tengah",
"nama": "Kepala Dusun Banjar Dinas Tengah",
"deskripsi": "Pimpinan wilayah Banjar Dinas Tengah",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_baler_pasar",
"nama": "Kepala Dusun Banjar Dinas Baler Pasar",
"deskripsi": "Pimpinan wilayah Banjar Dinas Baler Pasar",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_bucu",
"nama": "Kepala Dusun Banjar Dinas Bucu",
"deskripsi": "Pimpinan wilayah Banjar Dinas Bucu",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_gulingan",
"nama": "Kepala Dusun Banjar Dinas Gulingan",
"deskripsi": "Pimpinan wilayah Banjar Dinas Gulingan",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_bersih",
"nama": "Kepala Dusun Banjar Dinas Bersih",
"deskripsi": "Pimpinan wilayah Banjar Dinas Bersih",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_umahanyar",
"nama": "Kepala Dusun Banjar Dinas Umahanyar",
"deskripsi": "Pimpinan wilayah Banjar Dinas Umahanyar",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_taman",
"nama": "Kepala Dusun Banjar Dinas Taman",
"deskripsi": "Pimpinan wilayah Banjar Dinas Taman",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_darmasaba",
"nama": "Kepala Dusun Banjar Dinas Darmasaba",
"deskripsi": "Pimpinan wilayah Banjar Dinas Darmasaba",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "staf_desa",
"nama": "Staf Desa",
"deskripsi": "Staf Desa",
"hierarki": 3,
"parentId": "sekretaris_desa"
}
]
]

View File

@@ -1,27 +0,0 @@
[
{
"id": "kepala_desa",
"nama": "Kepala Desa",
"deskripsi": "Kepala Desa",
"hierarki": 1
},
{
"id": "sekretaris_desa",
"nama": "Sekretaris Desa",
"deskripsi": "Sekretaris Desa",
"hierarki": 2
},
{
"id": "bendahara_desa",
"nama": "Bendahara Desa",
"deskripsi": "Bendahara Desa",
"hierarki": 3
},
{
"id": "staff_umum",
"nama": "Staff Umum",
"deskripsi": "Staff Umum",
"hierarki": 4
}
]

View File

@@ -1,16 +1,4 @@
[
{
"id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
},
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",

View File

@@ -0,0 +1,6 @@
[
{ "nama": "Kebersihan" },
{ "nama": "Infrastruktur" },
{ "nama": "Sosial" },
{ "nama": "Lingkungan" }
]

View File

@@ -0,0 +1,9 @@
[
{ "id": "cmghqwjs4000404l8c5uvc300", "nama": "PAUD" },
{ "id": "cmghqwjs4000404l8c5uvc301", "nama": "TK" },
{ "id": "cmghqwjs4000404l8c5uvc302", "nama": "SD" },
{ "id": "cmghqwjs4000404l8c5uvc303", "nama": "SMP" },
{ "id": "cmghqwjs4000404l8c5uvc304", "nama": "SMA" },
{ "id": "cmghqwjs4000404l8c5uvc305", "nama": "SMK" }
]

View File

@@ -1,6 +1,6 @@
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"id": "cmgewz4gt000704ib91i3f169",
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
"gelarAkademik": "S.H.,M.H.,NL.P.",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
@@ -11,7 +11,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"id": "cmgewxfvw000004ibee5013f4",
"namaLengkap": "I Ketut Suwanta",
"gelarAkademik": "S.Pt",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -22,7 +22,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"id": "cmgewxvqw000104ibgm5l8fzs",
"namaLengkap": "Ni Wayan Supardiati",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -33,7 +33,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440011",
"id": "cmgewy1g9000204ib2n7hbx0i",
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -44,7 +44,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440012",
"id": "cmgewybah000304ibgqhn1gm2",
"namaLengkap": "I Wayan Sueca",
"gelarAkademik": "S.H.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -55,7 +55,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440017",
"id": "cmgewygqz000404ib20sv8nvg",
"namaLengkap": "Si Gede Ketut Astawa",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -66,7 +66,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440018",
"id": "cmgewyos1000504ibcu8o2gyk",
"namaLengkap": "I Kadek Arya Minarta",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -77,7 +77,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440021",
"id": "cmgewyxk7000604ib8djs3i6c",
"namaLengkap": "I Gede Andika Pradnya Diputra",
"gelarAkademik": "S.E.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",

View File

@@ -81,7 +81,7 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[]
Pegawai Pegawai[]
PegawaiBumDes PegawaiBumDes[]
DesaDigital DesaDigital[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
@@ -101,6 +101,7 @@ model FileStorage {
MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
StrukturBumDes StrukturBumDes[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -142,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true)
}
//========================================= PROFILE ========================================= //
//========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique
@@ -286,49 +287,51 @@ model StrukturPPID {
}
model PosisiOrganisasiPPID {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model PegawaiPPID {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model StrukturOrganisasiPPID {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
isActive Boolean @default(true)
}
// ========================================= VISI MISI PPID ========================================= //
@@ -850,7 +853,7 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1394,79 +1397,67 @@ model LowonganPekerjaan {
// ========================================= STRUKTUR ORGANISASI ========================================= //
model PosisiOrganisasi {
id String @id @default(uuid()) @db.VarChar(50)
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi")
model StrukturBumDes {
id String @id @default(cuid())
name 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)
PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id])
posisiOrganisasiBumDesId String?
PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id])
pegawaiBumDesId String?
}
model Pegawai {
id String @id @default(uuid()) @db.Uuid
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id])
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@map("pegawai")
model PosisiOrganisasiBumDes {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiBumDes[]
strukturOrganisasi StrukturBumDes[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiBumDes[] @relation("Parent")
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model HubunganOrganisasi {
id String @id @default(uuid()) @db.Uuid
atasanId String @db.Uuid
bawahanId String @db.Uuid
tipe String? @db.VarChar(50)
atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id])
bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id])
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@unique([atasanId, bawahanId])
@@map("hubungan_organisasi")
model PegawaiBumDes {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturBumDes[] // Relasi balik
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model StrukturOrganisasi {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
@@map("struktur_organisasi")
model StrukturOrganisasiBumDes {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
@@ -1615,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
@@ -2096,6 +2087,9 @@ model DataPerpustakaan {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
// relasi baru ke peminjaman
peminjamanBuku PeminjamanBuku[]
}
model KategoriBuku {
@@ -2108,6 +2102,31 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[]
}
model PeminjamanBuku {
id String @id @default(cuid())
nama String
noTelp String
alamat String
bukuId String
tanggalPinjam DateTime @default(now())
batasKembali DateTime // tenggat waktu pengembalian
tanggalKembali DateTime? // diisi saat dikembalikan
status StatusPeminjaman @default(Dipinjam)
catatan String? // opsional, misal: kondisi buku
buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
enum StatusPeminjaman {
Dipinjam
Dikembalikan
Terlambat
Dibatalkan
}
// ========================================= USER ========================================= //
model User {

View File

@@ -31,14 +31,14 @@ import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import kategoriBerita from "./data/kategori-berita.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
import kategoriBerita from "./data/desa/berita/kategori-berita.json";
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
@@ -56,6 +56,7 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
import roles from "./data/user/roles.json";
import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
import seedAssets from "./seed_assets";
import { safeSeedUnique } from "./safeseedUnique";
@@ -807,28 +808,34 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("kategori produk success ...");
for (const p of posisiOrganisasi) {
await prisma.posisiOrganisasi.upsert({
where: {
id: p.id,
},
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
const flattenedPosisiBumdes = posisiOrganisasi.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisiBumdes) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
);
continue;
}
}
await prisma.posisiOrganisasiBumDes.upsert({
where: { id: p.id },
update: p,
create: p,
});
}
console.log("posisi organisasi success ...");
console.log("posisi organisasi berhasil");
for (const p of pegawai) {
await prisma.pegawai.upsert({
await prisma.pegawaiBumDes.upsert({
where: {
id: p.id,
},
@@ -857,26 +864,6 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("pegawai success ...");
for (const p of hubunganOrganisasi) {
await prisma.hubunganOrganisasi.upsert({
where: {
atasanId_bawahanId: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
},
},
update: {
tipe: p.tipe,
},
create: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
tipe: p.tipe,
},
});
}
console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
@@ -900,6 +887,30 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("📊 detailDataPengangguran success ...");
// =========== KATEGORI GOTONG ROYONG ===========
// Add IDs to the kategoriKegiatan data
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
...k,
id: `kategori-${index + 1}`
}));
for (const k of kategoriKegiatan) {
await prisma.kategoriKegiatan.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("kategori kegiatan success ...");
for (const e of tujuanEdukasiLingkungan) {
await prisma.tujuanEdukasiLingkungan.upsert({
where: {
@@ -1154,6 +1165,22 @@ import { safeSeedUnique } from "./safeseedUnique";
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
for (const j of jenjangPendidikan) {
await prisma.jenjangPendidikan.upsert({
where: {
id: j.id || undefined,
},
update: {
nama: j.nama,
},
create: {
nama: j.nama,
},
});
}
console.log("✅ Jenjang Pendidikan seeded successfully");
// seed assets
await seedAssets();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

@@ -75,17 +75,18 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
berita.findMany.loading = false;
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
}
},
},
},
findUnique: {

View File

@@ -1,9 +1,173 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { proxy } from "valtio";
import { z } from "zod";
import { toast } from "react-toastify";
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, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const defaultForm = {
name: "",
imageId: "",
};
type StrukturBumDesForm = Prisma.StrukturBumDesGetPayload<{
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
const stateStruktur = proxy({
struktur: {
data: null as StrukturBumDesForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/${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;
} else {
throw new Error(result.message || "Gagal mengambil data struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
},
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(strukturData: StrukturBumDesForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.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/ekonomi/struktur-organisasi/${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 struktur");
await stateStruktur.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
return strukturData;
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
},
});
const templatePosisiOrganisasi = z.object({
nama: z.string().min(1, "Nama harus diisi"),
@@ -30,9 +194,7 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["create"].post(this.form);
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
@@ -52,6 +214,29 @@ const posisiOrganisasi = proxy({
},
},
findUnique: {
data: null as Prisma.StrukturOrganisasiBumDesGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`
);
if (res.ok) {
const data = await res.json();
posisiOrganisasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
posisiOrganisasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posisiOrganisasi:", error);
posisiOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...posisiOrganisasiDefaultForm },
@@ -165,17 +350,17 @@ const posisiOrganisasi = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
load: async (page = 1, limit?: number, search = "") => {
const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try {
const query: any = { page, limit };
const query: any = { page, limit: appliedLimit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many'].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
@@ -192,7 +377,42 @@ const posisiOrganisasi = proxy({
}
},
},
findManyAll: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
posisiOrganisasi.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many-all'].get({
query,
});
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load posisiOrganisasi:", res.data?.message);
posisiOrganisasi.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading posisiOrganisasi:", error);
posisiOrganisasi.findManyAll.data = [];
} finally {
posisiOrganisasi.findManyAll.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -231,12 +451,12 @@ const posisiOrganisasi = proxy({
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
telepon: z.string().min(1, "Telepom wajib diisi"),
alamat: z.string().min(1, "Alamat wajib diisi"),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
@@ -267,9 +487,9 @@ const pegawai = proxy({
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["create"].post(pegawai.create.form);
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['create'].post(
pegawai.create.form
);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
@@ -286,45 +506,56 @@ const pegawai = proxy({
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["find-many"].get({
query: { page, limit },
});
findMany: {
data: null as
| Prisma.PegawaiBumDesGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
pegawai.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['find-many'].get({
query,
});
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
| (Prisma.PegawaiBumDesGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
@@ -350,12 +581,9 @@ findMany: {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`,
{
method: "DELETE",
}
);
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
method: "DELETE",
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
@@ -372,6 +600,31 @@ findMany: {
},
},
nonActive: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.nonActive.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/non-active/${id}`, {
method: "DELETE", // biasanya nonActive pakai PATCH
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
await pegawai.findMany.load(); // refresh data
} else {
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
}
} catch (error) {
console.error("Gagal nonActive:", error);
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
} finally {
pegawai.nonActive.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
@@ -384,15 +637,12 @@ findMany: {
}
try {
const response = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -503,299 +753,10 @@ findMany: {
},
});
// Schema Zod untuk form validasi
const templateHubunganOrganisasiForm = z.object({
atasanId: z.string().min(1, "Atasan wajib dipilih"),
bawahanId: z.string().min(1, "Bawahan wajib dipilih"),
tipe: z.string().optional(),
});
// Default form state
const defaultHubunganOrganisasiForm = {
atasanId: "",
bawahanId: "",
tipe: "",
};
// ====================== STATE ===========================
const hubunganOrganisasi = proxy({
create: {
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async create() {
const cek = templateHubunganOrganisasiForm.safeParse(
hubunganOrganisasi.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
hubunganOrganisasi.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["create"].post(hubunganOrganisasi.create.form);
if (res.status === 200 && res.data?.success) {
hubunganOrganisasi.findMany.load();
return toast.success("Berhasil menambahkan hubungan organisasi");
} else {
return toast.error(res.data?.message || "Gagal menambahkan data");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan");
} finally {
hubunganOrganisasi.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
}> | null,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["find-many"].get();
if (res.status === 200) {
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map(
(item: any) => ({
...item,
atasan: item.atasan
? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true,
}
: null,
bawahan: item.bawahan
? {
...item.bawahan,
isActive:
item.bawahan.isActive ?? item.bawahan.aktif ?? true,
}
: null,
})
);
} else {
hubunganOrganisasi.findMany.data = [];
}
} catch (error) {
console.error("Fetch list error:", error);
toast.error("Gagal memuat data hubungan organisasi");
hubunganOrganisasi.findMany.data = [];
}
},
},
findUnique: {
data: null as {
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
} | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
hubunganOrganisasi.findUnique.data = result.data;
} else {
hubunganOrganisasi.findUnique.data = null;
toast.error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Find unique error:", error);
hubunganOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async load(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateHubunganOrganisasiForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
this.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await res.json();
if (res.ok && result.success) {
await hubunganOrganisasi.findMany.load();
toast.success("Berhasil mengupdate hubungan organisasi");
return true;
} else {
throw new Error(result?.message || "Gagal mengupdate");
}
} catch (error) {
console.error("Update error:", error);
toast.error(error instanceof Error ? error.message : "Gagal update");
return false;
} finally {
this.loading = false;
}
},
reset() {
hubunganOrganisasi.edit.id = "";
hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
hubunganOrganisasi.delete.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`,
{
method: "DELETE",
}
);
const result = await res.json();
if (res.ok && result?.success) {
toast.success("Hubungan organisasi berhasil dihapus");
hubunganOrganisasi.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus hubungan organisasi");
}
} catch (error) {
console.error("Delete error:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
hubunganOrganisasi.delete.loading = false;
}
},
},
});
const strukturorganisasiState = proxy({
const stateStrukturBumDes = proxy({
stateStruktur,
posisiOrganisasi,
pegawai,
hubunganOrganisasi,
});
export default strukturorganisasiState;
export default stateStrukturBumDes;

View File

@@ -332,7 +332,7 @@ const keunggulanProgram = proxy({
].post(keunggulanProgram.create.form);
if (res.status === 200) {
keunggulanProgram.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
return toast.success("Data Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");

View File

@@ -55,46 +55,95 @@ const dataPerpustakaan = proxy({
},
},
findMany: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
const startTime = Date.now();
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
dataPerpustakaan.findMany.loading = false;
}
},
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false;
}, delay);
}
},
},
findManyAll: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search;
try {
const query: any = {};
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findManyAll"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
} else {
dataPerpustakaan.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
@@ -321,17 +370,20 @@ const kategoriBuku = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
load: async (page = 1, limit = 10, search = "") => {
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
@@ -514,9 +566,319 @@ const kategoriBuku = proxy({
},
});
const templatePeminjamanBuku = z.object({
nama: z.string().min(1, "Nama harus diisi"),
noTelp: z.string().min(1, "No Telp harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
bukuId: z.string().min(1, "Buku ID harus diisi"),
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
catatan: z.string().min(1, "Catatan harus diisi"),
});
const defaultPeminjamanBuku = {
nama: "",
noTelp: "",
alamat: "",
bukuId: "",
tanggalPinjam: "",
batasKembali: "",
tanggalKembali: "",
catatan: "",
};
interface FormEditData {
nama: string;
noTelp: string;
alamat: string;
bukuId: string;
buku?: {
id: string;
judul: string;
};
tanggalPinjam: string;
batasKembali: string;
tanggalKembali: string;
catatan: string;
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
}
const editForm: FormEditData = {
nama: "",
noTelp: "",
alamat: "",
bukuId: "",
tanggalPinjam: "",
batasKembali: "",
tanggalKembali: "",
catatan: "",
status: "Dipinjam",
};
const peminjamanBuku = proxy({
create: {
form: { ...defaultPeminjamanBuku },
loading: false,
async create() {
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
peminjamanBuku.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"create"
].post(peminjamanBuku.create.form);
if (res.status === 200) {
peminjamanBuku.findMany.load();
return toast.success("Data Peminjaman Buku Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
peminjamanBuku.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.PeminjamanBukuGetPayload<{
include: {
buku: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
peminjamanBuku.findMany.page = page;
peminjamanBuku.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
peminjamanBuku.findMany.data = res.data.data ?? [];
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
} else {
peminjamanBuku.findMany.data = [];
peminjamanBuku.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data peminjaman buku paginated:", err);
peminjamanBuku.findMany.data = [];
peminjamanBuku.findMany.totalPages = 1;
} finally {
peminjamanBuku.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PeminjamanBukuGetPayload<{
include: {
buku: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
);
if (res.ok) {
const data = await res.json();
peminjamanBuku.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
peminjamanBuku.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
peminjamanBuku.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
peminjamanBuku.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Peminjaman Buku berhasil dihapus"
);
await peminjamanBuku.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus Data Peminjaman Buku"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
} finally {
peminjamanBuku.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...editForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
noTelp: data.noTelp,
alamat: data.alamat,
bukuId: data.bukuId,
tanggalPinjam: data.tanggalPinjam,
batasKembali: data.batasKembali,
tanggalKembali: data.tanggalKembali,
catatan: data.catatan,
status: data.status,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading peminjaman buku:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
peminjamanBuku.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
noTelp: this.form.noTelp,
alamat: this.form.alamat,
bukuId: this.form.bukuId,
tanggalPinjam: this.form.tanggalPinjam,
batasKembali: this.form.batasKembali,
tanggalKembali: this.form.tanggalKembali,
catatan: this.form.catatan,
status: this.form.status,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data peminjaman buku");
await peminjamanBuku.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data peminjaman buku"
);
}
} catch (error) {
console.error("Error updating data peminjaman buku:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data peminjaman buku"
);
return false;
} finally {
peminjamanBuku.update.loading = false;
}
},
reset() {
peminjamanBuku.update.id = "";
peminjamanBuku.update.form = { ...editForm };
},
},
});
const perpustakaanDigitalState = proxy({
dataPerpustakaan,
kategoriBuku,
peminjamanBuku,
});
export default perpustakaanDigitalState;

View File

@@ -561,9 +561,48 @@ const pegawai = proxy({
}
},
},
findManyAll: {
data: null as
| Prisma.PegawaiPPIDGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
pegawai.findManyAll.loading = true; // Use the full path to access the property
pegawai.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many-all"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pegawai.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findManyAll.data = [];
} finally {
pegawai.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
| (Prisma.PegawaiPPIDGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,

View File

@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
);
useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1');
pelayananPendudukNonPermanen.findById.load('edit');
}, []);
if (!pelayananPendudukNonPermanen.findById.data) {

View File

@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
try {
setLoading(true);
// You should get the ID from your router query or params
const id = '1'; // Replace with actual ID or get from URL params
const id = 'edit'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) {
setError('Gagal memuat data');

View File

@@ -53,7 +53,7 @@ function EditPasarDesa() {
// load data awal
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
pasarState.kategoriProduk.findManyAll.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
@@ -245,7 +245,7 @@ function EditPasarDesa() {
value={formData.kategoriId}
onChange={(val) => handleChange('kategoriId', val)}
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
pasarState.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []

View File

@@ -30,7 +30,7 @@ export default function CreatePasarDesa() {
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
statePasar.kategoriProduk.findMany.load();
statePasar.kategoriProduk.findManyAll.load();
}, []);
const resetForm = () => {
@@ -201,7 +201,7 @@ export default function CreatePasarDesa() {
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => (statePasar.pasarDesa.create.form.kategoriId = val)}
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
statePasar.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => {
if (!id) return;
stateProgram.findUnique
.load(id)
.then(() => {
const loadData = async () => {
try {
await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
},
});
}
})
.catch((err) => {
} catch (err) {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}, [id, stateProgram.findUnique]);
}
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level
const handleChange = useCallback(

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
ScrollArea,
Stack,
Tabs,
TabsList,
@@ -9,28 +10,20 @@ import {
TabsTab,
Title,
Tooltip,
ScrollArea,
} from '@mantine/core';
import {
IconBuildingCommunity,
IconHierarchy,
IconUsers
} from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import {
IconHierarchy,
IconUsers,
IconGitBranch,
} from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />,
tooltip: "Kelola daftar posisi organisasi",
},
{
label: "Pegawai",
value: "pegawai",
@@ -39,12 +32,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
tooltip: "Kelola data pegawai BUMDesa",
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi",
icon: <IconGitBranch size={18} stroke={1.8} />,
tooltip: "Atur hubungan antar posisi organisasi",
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />,
tooltip: "Kelola daftar posisi organisasi",
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
icon: <IconBuildingCommunity size={18} stroke={1.8} />,
tooltip: "Kelola struktur organisasi BUMDesa"
}
];
const currentTab = tabs.find((tab) => tab.href === pathname);

View File

@@ -1,116 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function EditHubunganOrganisasi() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
// load data awal sekali aja
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
if (id) {
(async () => {
const data = await state.edit.load(id);
if (data) {
setForm({
atasanId: data.atasanId ?? '',
bawahanId: data.bawahanId ?? '',
tipe: data.tipe ?? '',
});
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleChange = (field: keyof typeof form, value: string) => {
setForm(prev => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn('Atasan dan bawahan harus diisi');
return;
}
// update global state cuma pas submit
state.edit.id = id;
state.edit.form = form;
const result = await state.edit.update();
if (result) {
toast.success('Data berhasil diperbarui');
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi'
);
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Edit Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={
pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []
}
value={form.atasanId}
onChange={val => handleChange('atasanId', val || '')}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={
pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []
}
value={form.bawahanId}
onChange={val => handleChange('bawahanId', val || '')}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
value={form.tipe}
onChange={e => handleChange('tipe', e.currentTarget.value)}
/>
<Button onClick={handleSubmit} color="blue">
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,78 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function CreateHubunganOrganisasi() {
const router = useRouter();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
}, []);
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
return;
}
state.create.form = form;
const result = await state.create.create();
if (result) {
toast.success("Hubungan Organisasi berhasil ditambahkan");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Create Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
defaultValue={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,130 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function HubunganOrganisasi() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Hubungan Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListHubunganOrganisasi search={search} />
</Box>
);
}
function ListHubunganOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.hubunganOrganisasi);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
useShallowEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const handleHapus = () => {
if (selectedId) {
stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.atasan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.bawahan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.tipe?.toLowerCase().includes(keyword)
);
});
if (!stateOrganisasi.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Hubungan Organisasi'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Atasan</TableTh>
<TableTh>Bawahan</TableTh>
<TableTh>Tipe</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData
.sort((a, b) =>
a.atasan?.namaLengkap.localeCompare(b.atasan?.namaLengkap)
)
.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.atasan?.namaLengkap}</TableTd>
<TableTd>{item.bawahan?.namaLengkap}</TableTd>
<TableTd>{item.tipe}</TableTd>
<TableTd>
<Button
bg="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/${item.id}`)}
>
<IconEdit size={25} />
</Button>
</TableTd>
<TableTd>
<Button
color="red"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus hubungan organisasi ini?'
/>
</Box>
);
}
export default HubunganOrganisasi;

View File

@@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
@@ -13,7 +14,8 @@ import {
Stack,
Text,
TextInput,
Title
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -22,30 +24,12 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface PreviewImage {
file?: File;
preview: string;
}
interface PegawaiFormData {
namaLengkap: string;
gelarAkademik: string;
imageId: string | null;
tanggalMasuk: string;
email: string;
telepon: string;
alamat: string;
posisiId: string;
isActive: boolean;
}
export default function EditPegawai() {
export default function EditPegawaiBumDes() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const [previewImage, setPreviewImage] = useState<PreviewImage | string | null>(null);
const [formData, setFormData] = useState<PegawaiFormData>({
const [formData, setFormData] = useState({
namaLengkap: '',
gelarAkademik: '',
imageId: '',
@@ -56,16 +40,10 @@ export default function EditPegawai() {
posisiId: '',
isActive: true,
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// helper buat update state formData
const updateForm = (field: keyof PegawaiFormData, value: any) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
// Format date to YYYY-MM-DD for date input
// Format date for <input type="date">
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
@@ -73,10 +51,10 @@ export default function EditPegawai() {
};
useEffect(() => {
strukturorganisasiState.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
await stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
@@ -95,9 +73,7 @@ export default function EditPegawai() {
}
} catch (error) {
console.error('Error loading pegawai:', error);
toast.error(
error instanceof Error ? error.message : 'Gagal mengambil data pegawai'
);
toast.error(error instanceof Error ? error.message : 'Gagal mengambil data pegawai');
}
};
@@ -107,204 +83,182 @@ export default function EditPegawai() {
const handleSubmit = async () => {
try {
if (!formData.namaLengkap.trim()) {
toast.error('Nama lengkap tidak boleh kosong');
return;
return toast.error('Nama lengkap tidak boleh kosong');
}
stateOrganisasi.edit.form = {
...formData,
namaLengkap: formData.namaLengkap.trim(),
gelarAkademik: formData.gelarAkademik.trim(),
imageId: formData.imageId ? formData.imageId.trim() : '',
tanggalMasuk: formData.tanggalMasuk.trim(),
email: formData.email.trim(),
telepon: formData.telepon.trim(),
alamat: formData.alamat.trim(),
posisiId: formData.posisiId.trim(),
};
// Update global state only on submit
const updatedForm = { ...formData };
if (id && !stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id;
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');
updatedForm.imageId = uploaded.id;
}
stateOrganisasi.edit.form = updatedForm;
if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'
);
}
if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai');
} catch (error) {
console.error('Error updating pegawai:', error);
toast.error(
error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'
);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
}
};
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 Data Pegawai</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama Lengkap */}
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={formData.namaLengkap}
onChange={(e) => updateForm('namaLengkap', e.target.value)}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
required
/>
{/* Gelar Akademik */}
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => updateForm('gelarAkademik', e.target.value)}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
{/* Foto Profil */}
<Box>
<Text fz={'md'} fw={'bold'}>
Gambar
</Text>
<Box>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file),
});
}
}}
maxSize={5 * 1024 ** 2}
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp'],
}}
>
<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>
<Text fw="bold" fz="sm" mb={6}>Foto Profil</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept><IconUpload size={48} color={colors['blue-button']} stroke={1.5} /></Dropzone.Accept>
<Dropzone.Reject><IconX size={48} color="red" stroke={1.5} /></Dropzone.Reject>
<Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>Seret gambar atau klik untuk memilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
</Stack>
</Group>
</Dropzone>
<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>
{previewImage && (
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={
typeof previewImage === 'string'
? previewImage
: previewImage?.preview
}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy"
/>
)}
</Box>
</Box>
)}
</Box>
{/* Tanggal Masuk */}
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => updateForm('tanggalMasuk', e.target.value)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
{/* Email */}
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
type="email"
placeholder="contoh@email.com"
value={formData.email}
onChange={(e) => updateForm('email', e.currentTarget.value)}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
{/* Telepon */}
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
placeholder="08123456789"
value={formData.telepon}
onChange={(e) => updateForm('telepon', e.currentTarget.value)}
onChange={(e) => setFormData({ ...formData, telepon: e.target.value })}
/>
{/* Alamat */}
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
placeholder="Jl. Contoh No. 123"
value={formData.alamat}
onChange={(e) => updateForm('alamat', e.currentTarget.value)}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
strukturorganisasiState.posisiOrganisasi.findMany.data?.map(
(p) => ({
value: p.id,
label: p.nama,
})
) || []
}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) updateForm('posisiId', value);
}}
/>
{/* Posisi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>Posisi</Text>
<Select
placeholder="Pilih posisi"
data={stateStrukturBumDes.posisiOrganisasi.findManyAll.data?.map(p => ({ value: p.id, label: p.nama })) || []}
value={formData.posisiId}
onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
searchable
clearable
/>
</Box>
<Select
label="Status Pegawai"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)}
onChange={(val) => updateForm('isActive', val === 'true')}
/>
{/* Status Pegawai */}
<Box>
<Text fw="bold" fz="sm" mb={6}>Status Pegawai</Text>
<Select
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)}
onChange={(val) => setFormData({ ...formData, isActive: val === 'true' })}
clearable
/>
</Box>
<Group>
<Button onClick={handleSubmit} color="blue">
{/* Submit Button */}
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>

View File

@@ -1,42 +1,56 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPegawai() {
const statePegawai = useProxy(strukturorganisasiState.pegawai)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const statePegawai = useProxy(stateStrukturBumDes.pegawai);
const [modalHapus, setModalHapus] = useState(false);
const [modalNonActive, setModalNonActive] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string)
}, [])
stateStrukturBumDes.posisiOrganisasi.findMany.load();
statePegawai.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
statePegawai.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
}
};
const handleNonActive = () => {
if (selectedId) {
statePegawai.nonActive.byId(selectedId);
setModalNonActive(false);
setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
};
if (!statePegawai.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = statePegawai.findUnique.data;
return (
<Box>
<Box mb={10}>
@@ -44,91 +58,125 @@ function DetailPegawai() {
<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 Pegawai</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="md">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gelar Akademik</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.gelarAkademik}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' loading="lazy" />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Masuk</Text>
<Text fz={"lg"}>
{statePegawai.findUnique.data?.tanggalMasuk
? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
: "-"}
<Text fz="lg" fw="bold">Nama Lengkap</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Email</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{statePegawai.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Telepon</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.telepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{statePegawai.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Posisi</Text>
<Stack gap={4}>
{statePegawai.findUnique.data?.posisi ? (
<Text fz={"lg"}>
{statePegawai.findUnique.data.posisi.nama}
</Text>
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada posisi
</Text>
)}
</Stack>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Aktif</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id);
setModalHapus(true);
}
<Text fz="lg" fw="bold">Posisi</Text>
<Text fz="md" c="dimmed">{data.posisi?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Email</Text>
<Text fz="md" c="dimmed">{data.email || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Telepon</Text>
<Text fz="md" c="dimmed">{data.telepon || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tanggal Masuk</Text>
<Text fz="md" c="dimmed">
{data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Status</Text>
<Text fz="md" c={data.isActive ? 'green' : 'red'}>
{data.isActive ? 'Aktif' : 'Tidak Aktif'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Foto Profil</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.namaLengkap || 'Foto Profil'}
w={200}
h={200}
radius="md"
fit="cover"
mt="sm"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
disabled={!statePegawai.findUnique.data}
color="red">
<IconX size={20} />
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed" mt="sm">Tidak ada foto profil</Text>
)}
</Box>
<Button
color={data.isActive ? "green" : "gray"}
onClick={() => {
setSelectedId(data.id || null);
setModalNonActive(true);
}}
variant="light"
radius="md"
size="md"
>
{data.isActive ? "Aktif" : "Nonaktif"}
</Button>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Pegawai" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id || null);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Pegawai" withArrow position="top">
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}
color="green">
color="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
@@ -139,7 +187,15 @@ function DetailPegawai() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus produk ini?"
text="Apakah Anda yakin ingin menghapus data pegawai ini?"
/>
{/* Modal NonActive */}
<ModalKonfirmasiHapus
opened={modalNonActive}
onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive}
text="Apakah Anda yakin ingin menonaktifkan pegawai ini?"
/>
</Box>
);

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,17 +12,17 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePegawai() {
function CreatePegawaiBumDes() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState)
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai)
useEffect(() => {
stateOrganisasi.posisiOrganisasi.findMany.load();
stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
resetForm();
}, []);
const resetForm = () => {
stateOrganisasi.pegawai.create.form = {
stateOrganisasi.create.form = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
@@ -30,7 +31,7 @@ function CreatePegawai() {
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
isActive: true,
};
};
@@ -52,15 +53,15 @@ function CreatePegawai() {
}
// Set status aktif secara otomatis
stateOrganisasi.pegawai.create.form.isActive = true;
stateOrganisasi.create.form.isActive = true;
// Simpan ID gambar ke form
stateOrganisasi.pegawai.create.form.imageId = uploaded.id;
stateOrganisasi.create.form.imageId = uploaded.id;
// Submit form
await stateOrganisasi.pegawai.create.submit();
await stateOrganisasi.create.submit();
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
@@ -72,130 +73,194 @@ function CreatePegawai() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={stateOrganisasi.pegawai.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.pegawai.create.form.namaLengkap = e.currentTarget.value)}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={stateOrganisasi.pegawai.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.pegawai.create.form.gelarAkademik = e.currentTarget.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<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>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pegawai BUMDesa
</Title>
</Group>
<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>
{previewImage && (
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
required
/>
</Box>
<Box>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Foto Profil
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
styles={{
root: {
border: '2px dashed #ced4da',
borderRadius: '8px',
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: '#228be6',
},
},
}}
>
<Group justify="center" gap="xl" mih={160} 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="md" inline>
Seret gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Preview Gambar
</Text>
<Image
src={previewImage.preview}
alt="Preview"
width={280}
height={180}
width={200}
height={200}
fit="cover"
radius="sm"
mt="md"
loading="lazy"
radius="md"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
loading='lazy'
/>
)}
</Box>
</Box>
)}
</Box>
<Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Email"
type="email"
placeholder="Contoh: email@example.com"
defaultValue={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Nomor Telepon"
placeholder="Contoh: 08123456789"
defaultValue={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Posisi
</Text>
<Select
placeholder="Pilih posisi"
data={stateStrukturBumDes.posisiOrganisasi.findManyAll.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={stateOrganisasi.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.create.form.posisiId = value;
}}
searchable
clearable
/>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={stateOrganisasi.pegawai.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.pegawai.create.form.tanggalMasuk = e.currentTarget.value)}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
defaultValue={stateOrganisasi.pegawai.create.form.email}
onChange={(e) => (stateOrganisasi.pegawai.create.form.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
defaultValue={stateOrganisasi.pegawai.create.form.telepon}
onChange={(e) => (stateOrganisasi.pegawai.create.form.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={stateOrganisasi.pegawai.create.form.alamat}
onChange={(e) => (stateOrganisasi.pegawai.create.form.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateOrganisasi.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
defaultValue={stateOrganisasi.pegawai.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.pegawai.create.form.posisiId = value;
}}
searchable
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePegawai;
export default CreatePegawaiBumDes;

View File

@@ -1,33 +1,33 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function Pegawai() {
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai'
title='Pegawai BUMDesa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawai search={search} />
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawai({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const {
@@ -39,21 +39,10 @@ function ListPegawai({ search }: { search: string }) {
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, search);
}, [page, search]);
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap?.toLowerCase().includes(keyword) ||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
item.telepon?.toLowerCase().includes(keyword) ||
item.posisi?.nama?.toLowerCase().includes(keyword)
);
});
}, [data, search]);
const filteredData = data || []
// Handle loading state
if (loading || !data) {
@@ -67,29 +56,51 @@ function ListPegawai({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" ta="center">
<p>Tidak ada data pegawai yang tersedia</p>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
/>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -106,10 +117,20 @@ function ListPegawai({ search }: { search: string }) {
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
@@ -131,8 +152,15 @@ function ListPegawai({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
@@ -140,21 +168,22 @@ function ListPegawai({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default Pegawai;
export default PegawaiBumDes;

View File

@@ -1,30 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } 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 EditPosisiOrganisasi() {
function EditPosisiOrganisasiBumDes() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const [formData, setFormData] = useState({
nama: '',
@@ -32,24 +24,27 @@ function EditPosisiOrganisasi() {
hierarki: 0,
});
// Load data awal sekali saja
useEffect(() => {
const loadPosisiOrganisasi = async () => {
if (!id) return;
// Fungsi generik untuk update formData
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
useEffect(() => {
if (!id) return;
const loadPosisiOrganisasi = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
if (data) {
stateOrganisasi.edit.id = id;
setFormData({
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
hierarki: data.hierarki ?? 0,
nama: data.nama || '',
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
});
}
} catch (error) {
console.error('Error loading posisi organisasi:', error);
} catch (err) {
console.error('Error loading posisi organisasi:', err);
toast.error('Gagal memuat data posisi organisasi');
}
};
@@ -57,18 +52,14 @@ function EditPosisiOrganisasi() {
loadPosisiOrganisasi();
}, [id]);
const handleChange = (field: string, value: string | number) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama posisi organisasi tidak boleh kosong');
return;
}
if (!formData.nama.trim()) {
toast.error('Nama posisi organisasi tidak boleh kosong');
return;
}
// update global state HANYA saat submit
try {
// Update global state hanya saat submit
stateOrganisasi.edit.form = {
nama: formData.nama.trim(),
deskripsi: formData.deskripsi.trim(),
@@ -76,19 +67,17 @@ function EditPosisiOrganisasi() {
};
if (!stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id; // fallback
stateOrganisasi.edit.id = id;
}
const success = await stateOrganisasi.edit.update();
if (success) {
toast.success('Posisi organisasi berhasil diperbarui!');
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
}
} catch (error) {
console.error('Error updating posisi organisasi:', error);
} catch (err) {
console.error('Error updating posisi organisasi:', err);
// toast error biasanya sudah ada di update
}
};
@@ -96,17 +85,12 @@ function EditPosisiOrganisasi() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Posisi Organisasi
Edit Posisi Organisasi BUMDes
</Title>
</Group>
@@ -120,39 +104,40 @@ function EditPosisiOrganisasi() {
>
<Stack gap="md">
<TextInput
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
<Box>
<Text fz="sm" mb={6}>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
handleChange('deskripsi', htmlContent)
}
onChange={(html) => handleChange('deskripsi', html)}
/>
</Box>
<TextInput
type="number"
value={formData.hierarki}
onChange={(e) =>
handleChange('hierarki', parseInt(e.target.value) || 0)
}
label="Hierarki"
placeholder="Masukkan hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
value={formData.hierarki}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
handleChange('hierarki', isNaN(value) ? 0 : value);
}}
required
/>
<Group justify="right">
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
@@ -170,4 +155,4 @@ function EditPosisiOrganisasi() {
);
}
export default EditPosisiOrganisasi;
export default EditPosisiOrganisasiBumDes;

View File

@@ -1,128 +1,127 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasi() {
const router = useRouter();
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const resetForm = () => {
stateOrganisasi.create.form = {
nama: '',
deskripsi: '',
hierarki: 0,
};
function CreatePosisiOrganisasiBumDes() {
const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
useEffect(() => {
stateOrganisasi.findMany.load();
// Initialize form with default values
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
const handleSubmit = async () => {
await stateOrganisasi.create.submit();
resetForm();
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
return () => {
// Clean up form on unmount
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
};
}, []);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Posisi Organisasi
</Title>
</Group>
const handleSubmit = async () => {
try {
if (!stateOrganisasi.create.form.nama.trim()) {
return toast.error('Nama posisi tidak boleh kosong');
}
await stateOrganisasi.create.submit();
toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
} catch (error) {
toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error);
}
};
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Posisi Organisasi BUMDes
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
defaultValue={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
defaultValue={stateOrganisasi.create.form.hierarki || ''}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
}}
required
/>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.create.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
<Stack gap="md">
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
defaultValue={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
required
/>
<Box>
<Text fz={"sm"} mb={6}>Deskripsi</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
placeholder="Contoh: 1"
defaultValue={stateOrganisasi.create.form.hierarki}
onChange={(e) => {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value)) {
stateOrganisasi.create.form.hierarki = value;
}
}}
required
/>
{/* Action Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePosisiOrganisasi;
export default CreatePosisiOrganisasiBumDes;

View File

@@ -1,58 +1,37 @@
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasi() {
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
{/* Search Bar */}
<HeaderSearch
title="Posisi Organisasi"
placeholder="Cari nama, deskripsi, atau hierarki..."
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
{/* List Table */}
<ListPosisiOrganisasi search={search} />
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
@@ -66,21 +45,20 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
load(page, 10, search);
}, [page, search]);
const filteredData = data || [];
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search); // refresh
setModalHapus(false)
setSelectedId(null)
}
};
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
<Skeleton height={600} radius="md" />
</Stack>
);
}
@@ -89,71 +67,70 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi</Title>
<Tooltip label="Tambah Posisi Baru" withArrow>
<Title order={4}>Daftar Posisi Organisasi BumDes</Title>
<Tooltip label="Tambah Posisi Organisasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
)
}
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh>
<TableTh style={{ width: '25%' }}>Edit</TableTh>
<TableTh style={{ width: '25%' }}>Hapus</TableTh>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Text fz="sm">{item.hierarki ?? '-'}</Text>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Ubah Posisi Organisasi" withArrow>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`
)
}
size="sm"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Hapus Posisi Organisasi" withArrow>
<Button
variant="light"
color="red"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
@@ -170,8 +147,6 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -186,16 +161,15 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
radius="md"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi ini?"
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasi;
export default PosisiOrganisasiBumDes;

View File

@@ -0,0 +1,133 @@
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core';
import { IconUsers } from '@tabler/icons-react';
import { OrganizationChart } from 'primereact/organizationchart';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function StrukturOrganisasiBumDes() {
return (
<Box py="md">
<ListStrukturOrganisasiBumDes />
</Box>
);
}
function ListStrukturOrganisasiBumDes() {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
if (stateOrganisasi.findMany.loading) {
return (
<Center py={40}>
<Loader size="lg" />
</Center>
);
}
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
<Text fw={500} c="dimmed">Belum ada struktur organisasi yang ditambahkan</Text>
</Stack>
);
}
const posisiMap = new Map<string, any>();
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
let root: any[] = [];
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
}
} else {
root.push(posisi);
}
});
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
status: node.nama,
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
},
children: node.children.map(toOrgChartFormat),
};
}
const chartData = root.map(toOrgChartFormat);
return (
<Box py={10}>
<Paper
p="md"
radius="lg"
shadow="md"
withBorder
style={{
overflowX: 'auto',
background: 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)',
}}
>
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
</Paper>
</Box>
);
}
function nodeTemplate(node: any) {
const imageSrc = node?.data?.image || '/img/default.png';
const name = node?.data?.name || 'Tanpa Nama';
const status = node?.data?.status || 'Tidak ada deskripsi';
return (
<Stack align="center" gap={6} p="sm" style={{ minWidth: 160 }}>
<Tooltip label={name} position="top" withArrow>
<Image
src={imageSrc}
alt={name}
radius="xl"
w={100}
h={100}
fit="cover"
style={{
border: '1px solid #D3D1D1FF',
}}
loading='lazy'
/>
</Tooltip>
<Text fw={600} ta="center" size="sm">{name}</Text>
<Text size="xs" c="dimmed" ta="center">{status}</Text>
</Stack>
);
}
export default StrukturOrganisasiBumDes;

View File

@@ -4,7 +4,6 @@ import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
@@ -16,11 +15,10 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Ide Inovatif</Title>
<Tooltip label="Ajukan Ide Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Title order={4}>Daftar Ide Inovatif</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>

View File

@@ -1,10 +1,21 @@
'use client'
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -20,16 +31,14 @@ function EditDigitalSmartVillage() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// ✅ hanya lokal state untuk form
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
imageId: '',
});
// load data sekali saat mount
useEffect(() => {
const loadPenghargaan = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -42,69 +51,67 @@ function EditDigitalSmartVillage() {
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error("Error loading desa digital smart village:", error);
toast.error("Gagal memuat data desa digital smart village");
console.error('Error loading data:', error);
toast.error('Gagal memuat data desa digital smart village');
}
};
loadPenghargaan();
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
// ✅ update global state hanya saat submit
stateDesaDigital.edit.form = {
...stateDesaDigital.edit.form,
...formData,
};
stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
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");
}
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateDesaDigital.edit.form.imageId = uploaded.id;
}
await stateDesaDigital.edit.update();
toast.success("Desa digital smart village berhasil diperbarui!");
router.push("/admin/inovasi/desa-digital-smart-village");
toast.success('Desa digital smart village berhasil diperbarui!');
router.push('/admin/inovasi/desa-digital-smart-village');
} catch (error) {
console.error("Error updating desa digital smart village:", error);
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
console.error('Error updating desa digital:', error);
toast.error('Terjadi kesalahan saat memperbarui data');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Desa Digital Smart Village</Title>
{/* ✅ controlled input */}
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Desa Digital Smart Village
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '55%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Dropzone Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Text fw="bold" fz="sm" mb={6}>
Gambar Desa Digital
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
@@ -113,43 +120,43 @@ function EditDigitalSmartVillage() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</div>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview"
alt="Preview Gambar"
radius="md"
style={{
maxWidth: '100%',
maxHeight: '200px',
maxHeight: 220,
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
@@ -157,18 +164,43 @@ function EditDigitalSmartVillage() {
)}
</Box>
{/* Input Judul */}
<TextInput
label="Judul"
placeholder="Masukkan judul inovasi"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
{/* Editor Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
{/* ✅ controlled editor */}
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,8 +1,18 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,95 +20,136 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function DetailDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState)
const stateDesaDigital = useProxy(desaDigitalState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
stateDesaDigital.findUnique.load(params?.id as string)
}, [])
stateDesaDigital.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
stateDesaDigital.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/desa-digital-smart-village")
stateDesaDigital.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/desa-digital-smart-village");
}
}
};
if (!stateDesaDigital.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateDesaDigital.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
{stateDesaDigital.findUnique.data ? (
<Paper key={stateDesaDigital.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateDesaDigital.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateDesaDigital.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Desa Digital Smart Village
</Text>
{/* Sub Card Detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Desa Digital'}
w={200}
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateDesaDigital.findUnique.data) {
setSelectedId(stateDesaDigital.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
onClick={() => {
if (stateDesaDigital.findUnique.data) {
router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
}
}}
disabled={!stateDesaDigital.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal Konfirmasi */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
/>
</Box>
);

View File

@@ -22,7 +22,7 @@ import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
function CreateDesaDigital() {
export default function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -44,7 +44,6 @@ function CreateDesaDigital() {
}
try {
// Upload gambar dulu
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
@@ -55,10 +54,8 @@ function CreateDesaDigital() {
return toast.error('Gagal mengunggah gambar');
}
// Set imageId ke form
stateDesaDigital.create.form.imageId = uploaded.id;
// Submit form
const success = await stateDesaDigital.create.create();
if (success) {
resetForm();
@@ -72,10 +69,16 @@ function CreateDesaDigital() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
{/* Header dengan tombol kembali */}
<Group mb="md" align="center">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
style={{ transition: 'background 0.2s ease' }}
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -84,28 +87,32 @@ function CreateDesaDigital() {
</Title>
</Group>
{/* Card */}
{/* Card Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
p={{ base: 'md', md: 'xl' }}
radius="lg"
shadow="md"
style={{
border: '1px solid #eaeaea',
transition: 'box-shadow 0.3s ease',
}}
>
<Stack gap="md">
{/* Nama */}
<Stack gap="lg">
{/* Input Nama */}
<TextInput
label="Nama Desa Digital Smart Village"
placeholder="Masukkan nama desa digital smart village"
label="Nama Desa Digital"
placeholder="Masukkan nama desa digital"
defaultValue={stateDesaDigital.create.form.name}
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
required
radius="md"
withAsterisk
/>
{/* Deskripsi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
<Text fw={500} fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
@@ -118,8 +125,8 @@ function CreateDesaDigital() {
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar
<Text fw={500} fz="sm" mb={6}>
Gambar Desa Digital
</Text>
<Dropzone
onDrop={(files) => {
@@ -134,6 +141,11 @@ function CreateDesaDigital() {
accept={{ 'image/*': [] }}
radius="md"
p="xl"
style={{
border: '2px dashed #cfd8dc',
backgroundColor: '#fafafa',
transition: 'background-color 0.2s ease, border-color 0.2s ease',
}}
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
@@ -153,15 +165,22 @@ function CreateDesaDigital() {
{/* Preview */}
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box
mt="sm"
style={{
textAlign: 'center',
borderRadius: 12,
overflow: 'hidden',
}}
>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
maxHeight: 220,
objectFit: 'cover',
border: '1px solid #e0e0e0',
}}
loading="lazy"
/>
@@ -170,7 +189,7 @@ function CreateDesaDigital() {
</Box>
{/* Tombol Submit */}
<Group justify="right">
<Group justify="flex-end" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
@@ -179,6 +198,17 @@ function CreateDesaDigital() {
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
onMouseEnter={(e) => {
(e.currentTarget.style.transform = 'translateY(-2px)');
(e.currentTarget.style.boxShadow =
'0 6px 20px rgba(79, 172, 254, 0.5)');
}}
onMouseLeave={(e) => {
(e.currentTarget.style.transform = 'translateY(0)');
(e.currentTarget.style.boxShadow =
'0 4px 15px rgba(79, 172, 254, 0.4)');
}}
>
Simpan
@@ -189,5 +219,3 @@ function CreateDesaDigital() {
</Box>
);
}
export default CreateDesaDigital;

View File

@@ -55,7 +55,7 @@ function DetailInfoTeknologiTepatGuna() {
<Paper
withBorder
w={{ base: "100%", md: "70%", lg: "60%" }}
bg={colors['white-1']}
bg="#ECEEF8"
p="lg"
radius="md"
shadow="sm"

View File

@@ -1,9 +1,19 @@
'use client'
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -15,13 +25,11 @@ function EditJenisLayanan() {
const router = useRouter();
const params = useParams();
// state lokal untuk form
const [formData, setFormData] = useState({
nama: "",
deskripsi: "",
nama: '',
deskripsi: '',
});
// load data dari backend ke local state
useEffect(() => {
const loadJenisLayanan = async () => {
const id = params?.id as string;
@@ -31,20 +39,19 @@ function EditJenisLayanan() {
const data = await state.edit.load(id);
if (data) {
setFormData({
nama: data.nama ?? "",
deskripsi: data.deskripsi ?? "",
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
});
}
} catch (error) {
console.error("Error loading jenis layanan:", error);
toast.error("Gagal memuat data jenis layanan");
console.error('Error loading jenis layanan:', error);
toast.error('Gagal memuat data jenis layanan');
}
};
loadJenisLayanan();
}, [params?.id]);
// submit update → baru sync ke global state
const handleSubmit = async () => {
try {
state.edit.form = {
@@ -53,48 +60,85 @@ function EditJenisLayanan() {
};
await state.edit.update();
toast.success("Jenis layanan berhasil diperbarui!");
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan");
toast.success('Jenis layanan berhasil diperbarui!');
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
} catch (error) {
console.error("Error updating jenis layanan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
console.error('Error updating jenis layanan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p="md" w={{ base: "100%", md: "50%" }}>
<Stack gap="xs">
<Title order={3}>Edit Jenis Layanan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Layanan
</Title>
</Group>
{/* Form Container */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input: Nama Jenis Layanan */}
<TextInput
label="Nama Jenis Layanan"
placeholder="Masukkan nama jenis layanan"
value={formData.nama}
onChange={(e) =>
setFormData((prev) => ({ ...prev, nama: e.target.value }))
}
label={<Text fz="sm" fw="bold">Nama Jenis Layanan</Text>}
placeholder="masukkan nama jenis layanan"
required
/>
{/* Input: Deskripsi (Rich Text Editor) */}
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
setFormData((prev) => ({
...prev,
deskripsi: htmlContent,
}))
}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
{/* Tombol Submit */}
<Group justify="right" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -183,7 +183,7 @@ function EditArtikelKesehatan() {
{/* Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
Gambar Artikel Kesehatan
</Text>
<Dropzone
onDrop={handleFileChange}
@@ -240,15 +240,15 @@ function EditArtikelKesehatan() {
/>
{/* Pendahuluan */}
<InputText
label="Pendahuluan"
value={formData.introduction.content}
onChange={(value) =>
setFormData((prev) => ({ ...prev, introduction: { content: value } }))
}
placeholder="Masukkan pendahuluan"
/>
<Box>
<Text fw="bold">Pendahuluan</Text>
<EditEditor
value={formData.introduction.content}
onChange={(value) =>
setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } }))
}
/>
</Box>
{/* Gejala */}
<Box>
<Text fw="bold">Gejala</Text>

View File

@@ -115,7 +115,7 @@ function CreateArtikelKesehatan() {
<Stack gap="md">
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
Gambar Artikel Kesehatan
</Text>
<Dropzone
onDrop={(files) => {
@@ -163,7 +163,7 @@ function CreateArtikelKesehatan() {
</Box>
)}
</Box>
<TextInput
label={"Judul"}
placeholder="Masukkan judul"
@@ -182,16 +182,15 @@ function CreateArtikelKesehatan() {
}}
required
/>
<TextInput
label={"Pendahuluan"}
placeholder="Masukkan pendahuluan"
required
defaultValue={stateArtikelKesehatan.create.form.introduction.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.introduction.content = e.target.value;
}}
/>
<Box>
<Text fz="sm" fw="bold">Pendahuluan</Text>
<CreateEditor
value={stateArtikelKesehatan.create.form.introduction.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.introduction.content = e;
}}
/>
</Box>
{/* Gejala */}
<Box>
<Text fz="md" fw="bold">Gejala</Text>

View File

@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Konten</TableTh>
<TableTh>Aksi</TableTh>
<TableTh style={{ minWidth: 200 }}>Judul</TableTh>
<TableTh style={{ minWidth: 200 }}>Konten</TableTh>
<TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.title}
</Text>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }} >
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.content}
</Text>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Button
variant="light"
color="blue"

View File

@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper withBorder bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? (
<Center>

View File

@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</TableTd>
<TableTd>
<Box w={200}>
<Text truncate fz="sm" c="dimmed">
{item.deskripsiSingkat}
</Text>
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd>
<TableTd>

View File

@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Profil Desa
Profile Desa
</Title>
<Tabs

View File

@@ -150,13 +150,13 @@ export default function EditKegiatanDesa() {
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
required
/>
<TextInput
value={formData.deskripsiSingkat}
label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>}
placeholder="masukkan deskripsi singkat kegiatan desa"
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm">Deskripsi Singkat Kegiatan Desa</Text>
<EditEditor
value={formData.deskripsiSingkat}
onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiSingkat: htmlContent }))}
/>
</Box>
<Select
label="Kategori Kegiatan"
data={gotongRoyongState.kategoriKegiatan.findMany.data?.map(k => ({

View File

@@ -85,7 +85,7 @@ function DetailKegiatanDesa() {
{/* Deskripsi Singkat */}
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsiSingkat || '-'}</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} />
</Box>
{/* Deskripsi Lengkap */}

View File

@@ -159,13 +159,15 @@ function CreateKegiatanDesa() {
onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
defaultValue={stateKegiatanDesa.create.form.deskripsiSingkat}
onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Singkat
</Text>
<CreateEditor
value={stateKegiatanDesa.create.form.deskripsiSingkat}
onChange={(val) => (stateKegiatanDesa.create.form.deskripsiSingkat = val)}
/>
</Box>
<TextInput
type="number"
min={0}

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconSchool, IconStar } from '@tabler/icons-react';
@@ -58,36 +58,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -99,22 +99,22 @@ function ListKeunggulanProgram({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Keunggulan Program</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
<TableTh style={{ minWidth: 200 }}>Nama Keunggulan Program</TableTh>
<TableTh style={{ minWidth: 200 }}>Deskripsi</TableTh>
<TableTh style={{ minWidth: 200 }}>Edit</TableTh>
<TableTh style={{ minWidth: 200 }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Text
fw={500}
truncate="end"
@@ -122,7 +122,7 @@ function ListKeunggulanProgram({ search }: { search: string }) {
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
@@ -138,7 +138,7 @@ function ListKeunggulanProgram({ search }: { search: string }) {
</Button>
</Tooltip>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconSchool, IconCalendar, IconBuildingCommunity } from '@tabler/icons-react';
@@ -65,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBuilding, IconChalkboard, IconMicroscope, IconSchool } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -72,30 +72,36 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
@@ -106,6 +112,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
minHeight: "60vh",
}}
>
{children}
@@ -121,4 +128,3 @@ export default LayoutTabs;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconSchool, IconMapPin, IconBook2 } from '@tabler/icons-react';
@@ -66,36 +66,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconBook2, IconCategory } from '@tabler/icons-react';
import { IconBook2, IconCategory, IconUser } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -25,6 +25,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Atur kategori untuk buku digital",
},
{
label: "Peminjam",
value: "peminjam",
href: "/admin/pendidikan/perpustakaan-digital/peminjam",
icon: <IconUser size={18} stroke={1.8} />,
tooltip: "Data Peminjam Buku",
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
@@ -58,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -0,0 +1,254 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { useShallowEffect } from '@mantine/hooks';
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';
export type Status = "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
function EditPeminjam() {
const stateEdit = useProxy(perpustakaanDigitalState.peminjamanBuku);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState<{
nama: string;
noTelp: string;
alamat: string;
bukuId: string;
buku?: {
id: string;
judul: string;
};
tanggalPinjam: string;
batasKembali: string;
tanggalKembali: string;
status: Status;
catatan: string;
}>({
nama: '',
noTelp: '',
alamat: '',
bukuId: '',
tanggalPinjam: '',
batasKembali: '',
tanggalKembali: '',
status: 'Dipinjam', // Default status
catatan: '',
});
useShallowEffect(() => {
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
})
useEffect(() => {
const loadPeminjam = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateEdit.update.load(id);
if (data) {
setFormData((prev) => ({
...prev,
nama: data.nama ?? prev.nama,
noTelp: data.noTelp ?? prev.noTelp,
alamat: data.alamat ?? prev.alamat,
bukuId: data.bukuId ?? prev.bukuId,
buku: data.buku ? {
id: data.buku.id,
judul: data.buku.judul
} : undefined,
tanggalPinjam: data.tanggalPinjam ?? prev.tanggalPinjam,
batasKembali: data.batasKembali ?? prev.batasKembali,
tanggalKembali: data.tanggalKembali ?? prev.tanggalKembali,
status: (data.status as Status) ?? prev.status,
catatan: data.catatan ?? prev.catatan,
}));
}
} catch (error) {
console.error("Error loading peminjam:", error);
toast.error("Gagal mengambil data peminjam");
}
};
loadPeminjam();
}, [params?.id]);
const handleChange = (field: string, value: string | Status) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
stateEdit.update.form = {
...stateEdit.update.form,
...formData,
};
await stateEdit.update.update();
toast.success("Peminjam berhasil diperbarui!");
router.push("/admin/pendidikan/perpustakaan-digital/peminjam");
} catch (error) {
console.error("Error updating peminjam:", error);
toast.error("Terjadi kesalahan saat memperbarui peminjam");
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Peminjam Buku
</Title>
</Group>
{/* Card */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label={<Text fw="bold" fz="sm">Nama Peminjam</Text>}
placeholder="Masukkan nama peminjam"
required
/>
<TextInput
value={formData.noTelp}
onChange={(e) => handleChange('noTelp', e.target.value)}
label={<Text fw="bold" fz="sm">No Telp Peminjam</Text>}
placeholder="Masukkan no telp peminjam"
required
/>
<TextInput
value={formData.alamat}
onChange={(e) => handleChange('alamat', e.target.value)}
label={<Text fw="bold" fz="sm">Alamat Peminjam</Text>}
placeholder="Masukkan alamat peminjam"
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Buku</Text>
<Select
placeholder="Pilih buku"
data={perpustakaanDigitalState.dataPerpustakaan.findManyAll.data?.map(p => ({ value: p.id, label: p.judul })) || []}
value={formData.bukuId}
onChange={(value) => value && setFormData({ ...formData, bukuId: value })}
searchable
clearable
/>
</Box>
<DateInput
value={formData.tanggalPinjam}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.update.form.tanggalPinjam =
date ? new Date(date).toISOString() : '';
}}
label={<Text fw="bold" fz="sm">Tanggal Pinjam</Text>}
placeholder="Masukkan tanggal pinjam"
required
/>
<DateInput
value={formData.tanggalKembali}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.update.form.tanggalKembali =
date ? new Date(date).toISOString() : '';
}}
label={<Text fw="bold" fz="sm">Tanggal Kembali</Text>}
placeholder="Masukkan tanggal kembali"
required
/>
<DateInput
value={formData.batasKembali}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.update.form.batasKembali =
date ? new Date(date).toISOString() : '';
}}
label={<Text fw="bold" fz="sm">Batas Kembali</Text>}
placeholder="Masukkan batas kembali"
required
/>
<Select
value={formData.status}
onChange={(val) => handleChange('status', val as Status)}
label={<Text fw="bold" fz="sm">Status</Text>}
placeholder="Pilih status"
data={[
{ value: "Dipinjam", label: "Dipinjam" },
{ value: "Dikembalikan", label: "Dikembalikan" },
{ value: "Terlambat", label: "Terlambat" },
{ value: "Dibatalkan", label: "Dibatalkan" },
]}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Catatan</Text>
<EditEditor
value={formData.catatan}
onChange={(htmlContent) => handleChange('catatan', htmlContent)}
/>
</Box>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPeminjam;

View File

@@ -0,0 +1,237 @@
'use client';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailDataPeminjaman() {
const stateDetail = useProxy(perpustakaanDigitalState.peminjamanBuku);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string);
}, []);
const data = stateDetail.findUnique.data;
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.delete(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/pendidikan/perpustakaan-digital/peminjam');
}
};
const renderStatusBadge = (status: string) => {
const normalized = status?.toUpperCase();
switch (normalized) {
case 'DIPINJAM':
return <Badge color="blue" variant="light">Dipinjam</Badge>;
case 'DIKEMBALIKAN':
return <Badge color="green" variant="light">Dikembalikan</Badge>;
case 'TERLAMBAT':
return <Badge color="orange" variant="light">Terlambat</Badge>;
case 'DIBATALKAN':
return <Badge color="red" variant="light">Dibatalkan</Badge>;
default:
return <Badge color="gray" variant="light">Tidak diketahui</Badge>;
}
};
if (!data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Peminjam Buku
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Nama
</Text>
<Text fz="md" c="dimmed">
{data.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
No Telp
</Text>
<Text
fz="md"
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
>
{data.noTelp || '-'}
</Text>
</Box>
{/* 🔹 Editable Status */}
<Box>
<Group justify="space-between" align="end">
<Box>
<Text fz="lg" fw="bold">
Status
</Text>
{renderStatusBadge(data.status || '')}
</Box>
</Group>
</Box>
<Box>
<Text fz="lg" fw="bold">
Alamat
</Text>
<Text fz="md" c="dimmed">
{data.alamat || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Buku
</Text>
<Text fz="md" c="dimmed">
{data.buku?.judul || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Tanggal Pinjam
</Text>
<Text fz="md" c="dimmed">
{data.tanggalPinjam
? new Date(data.tanggalPinjam).toLocaleDateString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Tanggal Kembali
</Text>
<Text fz="md" c="dimmed">
{data.tanggalKembali
? new Date(data.tanggalKembali).toLocaleDateString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Batas Kembali
</Text>
<Text fz="md" c="dimmed">
{data.batasKembali
? new Date(data.batasKembali).toLocaleDateString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Catatan
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.catatan || '-' }}
/>
</Box>
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Peminjam Buku" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={stateDetail.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Peminjam Buku" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/pendidikan/perpustakaan-digital/peminjam/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data ini?"
/>
</Box>
);
}
export default DetailDataPeminjaman;

View File

@@ -0,0 +1,157 @@
'use client';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import perpustakaanDigitalState from '../../../_state/pendidikan/perpustakaan-digital';
function PeminjamBuku() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Peminjam Buku"
placeholder="Cari peminjam..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPeminjamBuku search={search} />
</Box>
);
}
function ListPeminjamBuku({ search }: { search: string }) {
const statePeminjam = useProxy(perpustakaanDigitalState.peminjamanBuku);
const router = useRouter();
const { data, page, totalPages, loading, load } = statePeminjam.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
const filteredData = data || [];
// 🔹 Fungsi helper untuk badge status
const renderStatusBadge = (status: string) => {
const normalized = status?.toUpperCase();
switch (normalized) {
case 'DIPINJAM':
return <Badge color="blue" variant="light">Dipinjam</Badge>;
case 'DIKEMBALIKAN':
return <Badge color="green" variant="light">Dikembalikan</Badge>;
case 'TERLAMBAT':
return <Badge color="orange" variant="light">Terlambat</Badge>;
case 'DIBATALKAN':
return <Badge color="red" variant="light">Dibatalkan</Badge>;
default:
return <Badge color="gray" variant="light">Tidak diketahui</Badge>;
}
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4} mb="md">Daftar Peminjam Buku</Title>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh style={{ width: '60%' }}>Nama Peminjam</TableTh>
<TableTh style={{ width: '15%' }}>Status</TableTh>
<TableTh style={{ width: '15%' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text truncate fz="sm">{item.nama}</Text>
</TableTd>
<TableTd>
{renderStatusBadge(item.status)}
</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/pendidikan/perpustakaan-digital/peminjam/${item.id}`)
}
>
<IconDeviceImacCog size={20} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
Tidak ada data Peminjam buku yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
)}
</Box>
);
}
export default PeminjamBuku;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconSchool, IconTarget } from '@tabler/icons-react';
@@ -11,13 +11,6 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const tabs = [
{
label: "Program Unggulan",
value: "program-unggulan",
href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan",
icon: <IconSchool size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola program unggulan pendidikan anak",
},
{
label: "Tujuan Program",
value: "tujuan-program",
@@ -25,6 +18,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
icon: <IconTarget size={18} stroke={1.8} />,
tooltip: "Atur tujuan program pendidikan anak",
},
{
label: "Program Unggulan",
value: "program-unggulan",
href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan",
icon: <IconSchool size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola program unggulan pendidikan anak",
}
];
const currentTab = tabs.find(tab => tab.href === pathname);
@@ -59,36 +59,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>

View File

@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
useEffect(() => {
stateOrganisasi.findMany.load();
stateOrganisasi.findManyAll.load();
}, []);
if (stateOrganisasi.findMany.loading) {
if (stateOrganisasi.findManyAll.loading) {
return (
<Center py={40}>
<Loader size="lg" />
@@ -32,7 +32,7 @@ function ListStrukturOrganisasiPPID() {
);
}
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
if (!stateOrganisasi.findManyAll.data || stateOrganisasi.findManyAll.data.length === 0) {
return (
<Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
@@ -43,7 +43,7 @@ function ListStrukturOrganisasiPPID() {
const posisiMap = new Map<string, any>();
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;

View File

@@ -225,7 +225,7 @@ export const navBar = [
{
id: "Ekonomi_3",
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
},
{
id: "Ekonomi_4",
@@ -377,22 +377,5 @@ export const navBar = [
path: "/admin/pendidikan/data-pendidikan"
}
]
},
{
id: "User & Role",
name: "User & Role",
path: "",
children: [
{
id: "User",
name: "User",
path: "/admin/user&role/user"
},
{
id: "Role",
name: "Role",
path: "/admin/user&role/role"
},
]
},
}
]

View File

@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon
onClick={() => {
router.push("/login");
router.push("/darmasaba");
}}
color={colors["blue-button"]}
radius="xl"

View File

@@ -1,11 +1,36 @@
import LayoutTabs from "./(dashboard)/landing-page/profile/_lib/layoutTabs";
import ProgramInovasi from "./(dashboard)/landing-page/profile/program-inovasi/page";
'use client';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
// Dynamically import the components with SSR disabled to prevent hydration issues
const LayoutTabs = dynamic(
() => import('./(dashboard)/landing-page/profile/_lib/layoutTabs'),
{ ssr: false }
);
const ProgramInovasi = dynamic(
() => import('./(dashboard)/landing-page/profile/program-inovasi/page'),
{ ssr: false }
);
export default function Page() {
const [mounted, setMounted] = useState(false);
// This ensures the component is only rendered on the client
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null; // or return a loading state
}
return (
<LayoutTabs>
<ProgramInovasi />
</LayoutTabs>
<div suppressHydrationWarning>
<LayoutTabs>
<ProgramInovasi />
</LayoutTabs>
</div>
)
}

View File

@@ -3,7 +3,6 @@ import PasarDesa from "./pasar-desa";
import LowonganKerja from "./lowongan-kerja";
import ProgramKemiskinan from "./program-kemiskinan";
import KategoriProduk from "./pasar-desa/kategori-produk";
import StrukturOrganisasi from "./struktur-organisasi";
import GrafikUsiaKerjaYangMenganggur from "./usia-kerja-yang-menganggur";
import GrafikMenganggurBerdasarkanPendidikan from "./usia-kerja-yang-menganggur/pengangguran-berdasrkan-pendidikan";
import JumlahPendudukMiskin from "./jumlah-penduduk-miskin";
@@ -11,6 +10,7 @@ import SektorUnggulanDesa from "./sektor-unggulan-desa";
import DemografiPekerjaan from "./demografi-pekerjaan";
import JumlahPengangguran from "./jumlah-pengangguran";
import PendapatanAsliDesa from "./pendapatan-asli-desa";
import StrukturOrganisasi from "./struktur-bumdes";
const Ekonomi = new Elysia({
prefix: "/api/ekonomi",

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
export default async function strukturBumDesFindById(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.strukturBumDes.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: "Berhasil mengambil data berdasarkan ID",
data,
}, { status: 200 });
} catch (e) {
console.error("Find by ID error:", e);
return Response.json({
success: false,
message: "Gagal mengambil data: " + (e instanceof Error ? e.message : 'Unknown error'),
}, {
status: 500,
});
}
}

View File

@@ -0,0 +1,29 @@
import Elysia, { t } from "elysia";
import PosisiOrganisasi from "./posisi-organisasi";
import Pegawai from "./pegawai";
import strukturBumDesFindById from "./find-by-id";
import strukturBumDesUpdate from "./update";
const StrukturOrganisasi = new Elysia({
prefix: "/struktur-organisasi",
tags: ["Ekonomi/Struktur Organisasi"],
})
.get("/:id", async (context) => {
const response = await strukturBumDesFindById(new Request(context.request))
return response
})
.use(PosisiOrganisasi)
.use(Pegawai)
.put("/:id", async (context) => {
const response = await strukturBumDesUpdate(context)
return response
}, {
body: t.Object({
name: t.String(),
imageId: t.String(),
})
})
export default StrukturOrganisasi;

View File

@@ -32,7 +32,7 @@ export default async function pegawaiCreate(context: Context) {
}
try {
const pegawai = await prisma.pegawai.create({
const pegawai = await prisma.pegawaiBumDes.create({
data: {
namaLengkap: body.namaLengkap,
gelarAkademik: body.gelarAkademik,

View File

@@ -13,17 +13,13 @@ export default async function pegawaiDelete(context: Context) {
}
try {
const deleted = await prisma.pegawai.update({
where: { id },
data: {
isActive: false, // soft delete
updatedAt: new Date(),
},
const deleted = await prisma.pegawaiBumDes.delete({
where: { id },
});
return {
success: true,
message: "Pegawai berhasil di-nonaktifkan",
message: "Pegawai berhasil dihapus",
data: deleted,
};
} catch (error: any) {

View File

@@ -9,7 +9,7 @@ export default async function pegawaiFindMany(context: Context) {
try {
const [data, total] = await Promise.all([
prisma.pegawai.findMany({
prisma.pegawaiBumDes.findMany({
where: { isActive: true },
include: {
posisi: true,
@@ -19,7 +19,7 @@ export default async function pegawaiFindMany(context: Context) {
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.pegawai.count({
prisma.pegawaiBumDes.count({
where: { isActive: true }
})
]);

View File

@@ -13,7 +13,7 @@ export default async function pegawaiFindUnique(context: Context) {
}
try {
const pegawai = await prisma.pegawai.findUnique({
const pegawai = await prisma.pegawaiBumDes.findUnique({
where: { id },
include: {
posisi: true,

View File

@@ -7,7 +7,7 @@ import pegawaiUpdate from "./updt";
const Pegawai = new Elysia({
prefix: "/pegawai",
tags: ["Ekonomi/Struktur Organisasi/Pegawai"],
tags: ["Ekonomi/Struktur BUMDes/Pegawai"],
})
// ✅ Find all

View File

@@ -28,7 +28,7 @@ export default async function pegawaiUpdate(context: Context) {
body.isActive = String(body.isActive).toLowerCase() === 'true';
try {
const updated = await prisma.pegawai.update({
const updated = await prisma.pegawaiBumDes.update({
where: { id: body.id },
data: {
namaLengkap: body.namaLengkap,

View File

@@ -18,7 +18,7 @@ export default async function posisiOrganisasiCreate(context: Context) {
}
try {
const posisiOrganisasi = await prisma.posisiOrganisasi.create({
const posisiOrganisasi = await prisma.posisiOrganisasiBumDes.create({
data: {
nama: body.nama,
deskripsi: body.deskripsi,

View File

@@ -16,7 +16,7 @@ export default async function posisiOrganisasiDelete(context: Context) {
try {
// Check if the position exists first
const existing = await prisma.posisiOrganisasi.findUnique({
const existing = await prisma.posisiOrganisasiBumDes.findUnique({
where: { id },
});
@@ -31,7 +31,7 @@ export default async function posisiOrganisasiDelete(context: Context) {
}
// Check if there are any pegawai associated with this position
const pegawaiCount = await prisma.pegawai.count({
const pegawaiCount = await prisma.pegawaiBumDes.count({
where: { posisiId: id },
});
@@ -45,28 +45,8 @@ export default async function posisiOrganisasiDelete(context: Context) {
);
}
// Check if this position is used in any hubungan organisasi
const hubunganCount = await prisma.hubunganOrganisasi.count({
where: {
OR: [
{ atasanId: id },
{ bawahanId: id },
],
},
});
if (hubunganCount > 0) {
return new Response(
JSON.stringify({
success: false,
message: "Tidak dapat menghapus posisi yang masih terdaftar dalam struktur organisasi",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// If all checks pass, delete the position
const deleted = await prisma.posisiOrganisasi.delete({
const deleted = await prisma.posisiOrganisasiBumDes.delete({
where: { id },
});

View File

@@ -23,13 +23,13 @@ export default async function posisiOrganisasiFindMany(context: Context) {
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.posisiOrganisasi.findMany({
prisma.posisiOrganisasiBumDes.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "asc" },
}),
prisma.posisiOrganisasi.count({ where }),
prisma.posisiOrganisasiBumDes.count({ where }),
]);
return {

View File

@@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/posisi-organisasi/findManyAll.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function posisiOrganisasiFindManyAll(context: Context) {
const search = (context.query.search as string) || "";
// filter default
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ deskripsi: { contains: search, mode: "insensitive" } },
];
}
try {
const data = await prisma.posisiOrganisasiPPID.findMany({
where,
orderBy: { hierarki: "asc" },
});
return {
success: true,
message: "Berhasil mengambil semua data posisi organisasi",
data: data.map((item: any) => ({
id: item.id,
nama: item.nama,
deskripsi: item.deskripsi,
hierarki: item.hierarki,
})),
total: data.length,
};
} catch (e) {
console.error("Find many all error:", e);
return {
success: false,
message: "Gagal mengambil data posisi organisasi",
};
}
}
export default posisiOrganisasiFindManyAll;

View File

@@ -21,7 +21,7 @@ export default async function posisiOrganisasiFindUnique(context: Context) {
}
}
const data = await prisma.posisiOrganisasi.findUnique({
const data = await prisma.posisiOrganisasiBumDes.findUnique({
where: { id },
});

View File

@@ -4,6 +4,7 @@ import posisiOrganisasiFindUnique from "./findUnique";
import posisiOrganisasiCreate from "./create";
import posisiOrganisasiUpdate from "./updt";
import posisiOrganisasiDelete from "./del";
import posisiOrganisasiFindManyAll from "./findManyAll";
const PosisiOrganisasi = new Elysia({
prefix: "/posisi-organisasi",
@@ -11,6 +12,7 @@ const PosisiOrganisasi = new Elysia({
})
.get("/find-many", posisiOrganisasiFindMany)
.get("/find-many-all", posisiOrganisasiFindManyAll)
.get("/:id", async (context) => {
const response = await posisiOrganisasiFindUnique(context);
return response;

View File

@@ -20,7 +20,7 @@ export default async function posisiOrganisasiUpdate(context: Context) {
}
try {
await prisma.posisiOrganisasi.update({
await prisma.posisiOrganisasiBumDes.update({
where: { id },
data: {
nama: body.nama,
@@ -29,7 +29,7 @@ export default async function posisiOrganisasiUpdate(context: Context) {
},
});
const updated = await prisma.posisiOrganisasi.findUnique({
const updated = await prisma.posisiOrganisasiBumDes.findUnique({
where: { id },
});

View File

@@ -0,0 +1,116 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
import { Prisma } from "@prisma/client";
type FormUpdate = Prisma.StrukturBumDesGetPayload<{
select: {
id: true;
name: true;
imageId: true;
};
}>;
export default async function strukturBumDesUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, 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.strukturBumDes.findUnique({
where: {
id
},
include: {
image: true,
}
})
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Data tidak ditemukan",
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
)
}
if (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 (error) {
console.error("Gagal hapus gambar lama:", error);
}
}
}
const updated = await prisma.strukturBumDes.update({
where: {
id
},
data: {
name,
imageId,
}
})
return new Response(
JSON.stringify({
success: true,
message: "Struktur BumDes Berhasil Dibuat",
data: updated,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
)
} catch (error) {
console.error("Error updating struktur BumDes:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate struktur BumDes",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
)
}
}

View File

@@ -1,51 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreateHubunganOrganisasi = {
atasanId: string;
bawahanId: string;
tipe?: string;
};
export default async function hubunganOrganisasiCreate(context: Context) {
const body = await context.body as FormCreateHubunganOrganisasi;
// Validasi minimal
if (!body || !body.atasanId || !body.bawahanId) {
return {
success: false,
message: "atasanId dan bawahanId wajib diisi",
};
}
try {
const data = await prisma.hubunganOrganisasi.create({
data: {
atasanId: body.atasanId,
bawahanId: body.bawahanId,
tipe: body.tipe,
},
});
return {
success: true,
message: "Berhasil membuat hubungan organisasi",
data,
};
} catch (error: any) {
if (error.code === "P2002") {
return {
success: false,
message: "Hubungan antara atasan dan bawahan sudah ada",
};
}
console.error("Error create hubungan organisasi:", error);
return {
success: false,
message: "Gagal membuat hubungan organisasi",
error: error.message,
};
}
}

View File

@@ -1,33 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function hubunganOrganisasiDelete(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID wajib diisi",
};
}
try {
const deleted = await prisma.hubunganOrganisasi.delete({
where: { id },
});
return {
success: true,
message: "Hubungan organisasi berhasil dihapus",
data: deleted,
};
} catch (error: any) {
console.error("Error delete hubungan organisasi:", error);
return {
success: false,
message: "Gagal menghapus hubungan organisasi",
error: error.message,
};
}
}

View File

@@ -1,28 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
export default async function hubunganOrganisasiFindMany() {
try {
const data = await prisma.hubunganOrganisasi.findMany({
include: {
atasan: true,
bawahan: true,
},
orderBy: {
atasanId: "asc",
},
});
return {
success: true,
data,
};
} catch (error: any) {
console.error("Error findMany hubungan organisasi:", error);
return {
success: false,
message: "Gagal mengambil data hubungan organisasi",
error: error.message,
};
}
}

View File

@@ -1,43 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function hubunganOrganisasiFindUnique(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID hubungan organisasi wajib diisi",
};
}
try {
const data = await prisma.hubunganOrganisasi.findUnique({
where: { id },
include: {
atasan: true,
bawahan: true,
},
});
if (!data) {
return {
success: false,
message: "Data hubungan organisasi tidak ditemukan",
};
}
return {
success: true,
data,
};
} catch (error: any) {
console.error("Error findUnique hubungan organisasi:", error);
return {
success: false,
message: "Gagal mengambil data",
error: error.message,
};
}
}

View File

@@ -1,47 +0,0 @@
import Elysia, { t } from "elysia";
import hubunganOrganisasiFindMany from "./findMany";
import hubunganOrganisasiFindUnique from "./findUnique";
import hubunganOrganisasiCreate from "./create";
import hubunganOrganisasiUpdate from "./updt";
import hubunganOrganisasiDelete from "./del";
const HubunganOrganisasi = new Elysia({
prefix: "/hubungan-organisasi",
tags: ["Ekonomi/Struktur Organisasi/Hubungan Organisasi"],
})
// 🔍 GET /find-many
.get("/find-many", hubunganOrganisasiFindMany)
// 🔍 GET /:id
.get("/:id", async (context) => {
return await hubunganOrganisasiFindUnique(context);
})
// POST /create
.post("/create", hubunganOrganisasiCreate, {
body: t.Object({
atasanId: t.String(),
bawahanId: t.String(),
tipe: t.Optional(t.String()),
}),
})
// ✏️ PUT /:id
.put( "/:id",
async (context) => {
const response = await hubunganOrganisasiUpdate(context);
return response;
}, {
body: t.Object({
atasanId: t.Optional(t.String()),
bawahanId: t.Optional(t.String()),
tipe: t.Optional(t.String()),
}),
})
// ❌ DELETE /del/:id
.delete("/del/:id", hubunganOrganisasiDelete);
export default HubunganOrganisasi;

View File

@@ -1,52 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
/* eslint-disable @typescript-eslint/no-explicit-any */
type FormUpdateHubungan = {
atasanId?: string;
bawahanId?: string;
tipe?: string;
};
export default async function hubunganOrganisasiUpdate(context: Context) {
const body = await context.body as Omit<FormUpdateHubungan, 'id'>;
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID wajib ada di URL",
};
}
try {
const updated = await prisma.hubunganOrganisasi.update({
where: { id },
data: {
atasanId: body.atasanId,
bawahanId: body.bawahanId,
tipe: body.tipe,
},
});
return {
success: true,
message: "Hubungan organisasi berhasil diupdate",
data: updated,
};
} catch (error: any) {
if (error.code === "P2002") {
return {
success: false,
message: "Relasi atasan-bawahan sudah ada",
};
}
console.error("Error update hubungan organisasi:", error);
return {
success: false,
message: "Gagal update data hubungan organisasi",
error: error.message,
};
}
}

View File

@@ -1,14 +0,0 @@
import Elysia from "elysia";
import PosisiOrganisasi from "./posisi-organisasi";
import Pegawai from "./pegawai";
import HubunganOrganisasi from "./hubungan-organisasi";
const StrukturOrganisasi = new Elysia({
prefix: "/struktur-organisasi",
tags: ["Ekonomi/Struktur Organisasi"],
})
.use(PosisiOrganisasi)
.use(Pegawai)
.use(HubunganOrganisasi)
export default StrukturOrganisasi;

View File

@@ -9,7 +9,7 @@ export default async function KegiatanDesaFindFirst(context: Context) {
if (kategori) {
where.kategoriKegiatan = {
name: { equals: kategori, mode: 'insensitive' }
nama: { equals: kategori, mode: 'insensitive' }
};
}

View File

@@ -66,9 +66,6 @@ async function lembagaPendidikanFindMany(context: Context) {
})
]);
console.log('Fetched data count:', data.length);
console.log('Total count:', total);
return {
success: true,
message: "Success fetch lembaga pendidikan with pagination",

View File

@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataPerpustakaanFindManyAll(context: Context) {
const search = (context.query.search as string) || "";
const isActiveParam = context.query.isActive;
// Buat where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
if (search) {
where.OR = [
{ judul: { contains: search, mode: "insensitive" } },
{ deskripsi: { contains: search, mode: "insensitive" } },
];
}
try {
const data = await prisma.dataPerpustakaan.findMany({
where,
include: {
kategori: true,
image: true,
},
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: "Success fetch all data perpustakaan (non-paginated)",
total: data.length,
data,
};
} catch (error) {
console.error("Find many all error:", error);
return {
success: false,
message: "Failed fetch all data perpustakaan",
total: 0,
data: [],
};
}
}

View File

@@ -4,6 +4,7 @@ import dataPerpustakaanDelete from "./del";
import dataPerpustakaanFindMany from "./findMany";
import dataPerpustakaanFindUnique from "./findUnique";
import dataPerpustakaanUpdate from "./updt";
import dataPerpustakaanFindManyAll from "./findManyAll";
const DataPerpustakaan = new Elysia({
prefix: "/dataperpustakaan",
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
kategoriId: t.String(),
}),
})
.get("/findManyAll", dataPerpustakaanFindManyAll)
.get("/findMany", dataPerpustakaanFindMany)
.get("/:id", async (context) => {
const response = await dataPerpustakaanFindUnique(

View File

@@ -1,12 +1,14 @@
import Elysia from "elysia";
import DataPerpustakaan from "./data-perpustakaan";
import KategoriBuku from "./kategori-buku";
import PeminjamanBuku from "./peminjaman";
const PerpustakaanDigital = new Elysia({
prefix: "/perpustakaandigital",
tags: ["Pendidikan / Perpustakaan Digital"],
})
.use(DataPerpustakaan)
.use(KategoriBuku);
.use(KategoriBuku)
.use(PeminjamanBuku);
export default PerpustakaanDigital;

View File

@@ -0,0 +1,40 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
noTelp: string;
alamat: string;
bukuId: string;
tanggalPinjam: string;
batasKembali: string;
tanggalKembali?: string;
catatan?: string;
}
export default async function peminjamanBukuCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.peminjamanBuku.create({
data: {
nama: body.nama,
noTelp: body.noTelp,
alamat: body.alamat,
bukuId: body.bukuId,
tanggalPinjam: body.tanggalPinjam,
batasKembali: body.batasKembali,
tanggalKembali: body.tanggalKembali,
catatan: body.catatan,
},
});
return {
success: true,
message: "Berhasil membuat peminjaman buku",
data: result,
};
} catch (error) {
console.error("Error creating peminjaman buku:", error);
throw new Error("Gagal membuat peminjaman buku: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function peminjamanBukuDelete(context: Context) {
const id = context.params.id as string;
await prisma.peminjamanBuku.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete peminjaman buku",
};
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More