Compare commits

..

4 Commits

Author SHA1 Message Date
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
104 changed files with 5891 additions and 2895 deletions

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

@@ -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 ========================================= //
@@ -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 ========================================= //
@@ -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();

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

@@ -514,9 +514,312 @@ 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

@@ -563,7 +563,7 @@ const pegawai = proxy({
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
| (Prisma.PegawaiPPIDGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,

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

@@ -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,38 @@ 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)",
}}
>
{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",
}}
<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)",
}}
>
{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,38 @@ 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)",
}}
>
{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",
}}
<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)",
}}
>
{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,32 @@ 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)",
}}
>
{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 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)",
}}
>
{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 +108,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 +124,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,38 @@ 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)",
}}
>
{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",
}}
<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)",
}}
>
{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,38 @@ 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)",
}}
>
{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",
}}
<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)",
}}
>
{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,245 @@
/* 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 { 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: '',
});
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
/>
<TextInput
value={formData.buku?.judul || ''}
label={<Text fw="bold" fz="sm">Buku</Text>}
placeholder="Buku"
required
readOnly
/>
<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,38 @@ 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)",
}}
>
{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",
}}
<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)",
}}
>
{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

@@ -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",

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

@@ -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'),
}
}
}

View File

@@ -0,0 +1,48 @@
import Elysia, { t } from "elysia";
import peminjamanBukuCreate from "./create";
import peminjamanBukuDelete from "./del";
import peminjamanBukuFindMany from "./findMany";
import peminjamanBukuFindUnique from "./findUnique";
import peminjamanBukuUpdate from "./updt";
const PeminjamanBuku = new Elysia({
prefix: "/peminjamanbuku",
tags: ["Pendidikan / Perpustakaan Digital / Peminjaman Buku"],
})
.post("/create", peminjamanBukuCreate, {
body: t.Object({
nama: t.String(),
noTelp: t.String(),
alamat: t.String(),
bukuId: t.String(),
tanggalPinjam: t.String(),
batasKembali: t.String(),
tanggalKembali: t.String(),
catatan: t.String()
}),
})
.get("/findMany", peminjamanBukuFindMany)
.get("/:id", async (context) => {
const response = await peminjamanBukuFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", peminjamanBukuUpdate, {
body: t.Object({
nama: t.String(),
noTelp: t.String(),
alamat: t.String(),
bukuId: t.String(),
tanggalPinjam: t.String(),
batasKembali: t.String(),
tanggalKembali: t.String(),
catatan: t.String(),
status: t.String()
}),
})
.delete("/del/:id", peminjamanBukuDelete);
export default PeminjamanBuku;

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
nama: string;
noTelp: string;
alamat: string;
bukuId: string;
tanggalPinjam: string;
batasKembali: string;
tanggalKembali?: string;
catatan?: string;
status?: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
};
export default async function dataPerpustakaanUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.peminjamanBuku.update({
where: { id },
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,
status: body.status,
},
include: {
buku: true,
}
});
return {
success: true,
message: "Berhasil mengupdate data peminjaman buku",
data: result,
};
} catch (error) {
console.error("Error updating data peminjaman buku:", error);
throw new Error(
"Gagal mengupdate data peminjaman buku: " + (error as Error).message
);
}
}

View File

@@ -135,7 +135,7 @@ export default function Content({ kategori }: { kategori: string }) {
{item.kategoriBerita?.name || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {

View File

@@ -1,4 +1,3 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./Content";

View File

@@ -1,24 +1,300 @@
import colors from '@/con/colors';
import { Stack, Box, Text, Image } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors'
import {
Box,
Button,
Card,
Center,
Container,
Group,
Image,
Loader,
Paper,
Stack,
Text,
Title,
Tooltip,
Transition,
} from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
function Page() {
export default function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Text px={{ base: 'md', md: 100 }} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Struktur Organisasi dan SK Pengurus BUMDesa
</Text>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Image src={'/api/img/bpddarmasaba.png'} alt='' loading="lazy"/>
<Box
style={{
minHeight: '100vh',
background: colors['Bg'],
color: '#E6F0FF',
paddingBottom: 48,
}}
>
<Container size="xl" py="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Stack align="center" gap="xl" mt="xl">
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
>
Struktur Organisasi Dan SK Pengurus BumDes
</Title>
<Text ta="center" c="black" maw={800}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan.
</Text>
</Stack>
</Box>
</Stack>
);
<Box mt="lg">
<StrukturOrganisasiBumDes />
</Box>
</Container>
</Box>
)
}
export default Page;
function StrukturOrganisasiBumDes() {
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
useEffect(() => {
void stateOrganisasi.findMany.load()
}, [])
const isLoading =
!stateOrganisasi.findMany.data &&
stateOrganisasi.findMany.loading !== false
if (isLoading) {
return (
<Center py={48}>
<Stack align="center" gap="sm">
<Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm">
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
</Text>
</Stack>
</Center>
)
}
if (
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
return (
<Center py={40}>
<Stack align="center" gap="md">
<Paper
radius="md"
p="xl"
style={{
width: 560,
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
textAlign: 'center',
}}
>
<Center>
<IconUsers size={56} />
</Center>
<Title order={3} mt="md">
Data pegawai belum tersedia
</Title>
<Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba
muat ulang atau periksa sumber data.
</Text>
<Group justify="center" mt="lg">
<Button
leftSection={<IconRefresh size={16} />}
variant="gradient"
gradient={{ from: 'indigo', to: 'cyan' }}
onClick={() => stateOrganisasi.findMany.load()}
>
Muat Ulang
</Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group>
</Paper>
</Stack>
</Center>
)
}
const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => 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);
}
// First, create a map of all unique positions
const allPositions = new Map();
aktifPegawai.forEach((pegawai: any) => {
if (!allPositions.has(pegawai.posisi.id)) {
allPositions.set(pegawai.posisi.id, {
...pegawai.posisi,
pegawaiList: [],
children: []
});
}
});
// Then assign employees to their positions
aktifPegawai.forEach((pegawai: any) => {
const posisi = allPositions.get(pegawai.posisi.id);
if (posisi) {
posisi.pegawaiList.push(pegawai);
}
});
// Now build the hierarchy
const root = [];
for (const [_, posisi] of allPositions) {
if (posisi.parentId) {
const parent = allPositions.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
} else {
// Only add to root if it's a top-level position
if (!posisi.parentId) {
root.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 ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
},
children: node.children?.map(toOrgChartFormat) || [],
}
}
const chartData = root.map(toOrgChartFormat)
return (
<Box py={16} >
<Paper
radius="md"
p="md"
style={{
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
overflowX: 'auto',
}}
>
<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 title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
{(styles) => (
<Card
radius="lg"
withBorder
style={{
...styles,
width: 260,
padding: 16,
background: 'rgba(28,110,164,0.3)',
borderColor: 'rgba(255,255,255,0.15)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Image
src={imageSrc}
alt={name}
radius="md"
width={120}
height={120}
fit="cover"
style={{
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12,
}}
loading='lazy'
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>
{title}
</Text>
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.positionId
if (id && (window as any).scrollTo) {
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
}}
>
Kembali
</Button>
</Tooltip>
</Card>
)}
</Transition>
)
}

View File

@@ -46,7 +46,7 @@ function Page() {
useShallowEffect(() => {
mitraState.findMany.load(page, 10);
load(page, 10, search, selectedYear || '');
load(page, 10, search, selectedYear ? `year:${selectedYear}` : '');
}, [page, search, selectedYear]);
const mitraData = mitraState.findMany.data || [];

View File

@@ -1,48 +1,31 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, List, ListItem, Paper, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
const data = [
{
id: 1,
title: 'Tujuan Edukasi Lingkungan',
icon: <IconLeaf size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Meningkatkan kesadaran masyarakat akan pentingnya lingkungan bersih dan sehat',
'Mendorong partisipasi warga dalam pengelolaan sampah, penghijauan, dan konservasi',
'Mengurangi dampak negatif kegiatan manusia terhadap lingkungan',
'Membentuk generasi muda peduli isu-isu lingkungan',
],
},
{
id: 2,
title: 'Materi Edukasi yang Diberikan',
icon: <IconRecycle size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Pengelolaan sampah: pilah organik & anorganik',
'Pencegahan pencemaran lingkungan (air, udara, tanah)',
'Pemanfaatan lahan hijau dan penghijauan desa',
'Daur ulang dan kreativitas dari sampah',
'Bahaya pembakaran sampah sembarangan',
],
},
{
id: 3,
title: 'Contoh Kegiatan di Desa Darmasaba',
icon: <IconPlant2 size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Pelatihan membuat kompos dari sampah rumah tangga',
'Gerakan "Jumat Bersih" rutin',
'Workshop pembuatan ecobrick',
'Lomba kebersihan antar banjar',
'Sosialisasi lingkungan di sekolah dan posyandu',
],
},
];
function Page() {
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById)
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById)
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
useShallowEffect(() => {
tujuan.load('edit')
materi.load('edit')
contoh.load('edit')
}, [])
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) {
return (
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
@@ -60,28 +43,84 @@ function Page() {
</Box>
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{data.map((item) => (
<Paper key={item.id} p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
{/* Tujuan Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack gap="md">
<Box>
<Tooltip label={item.title} position="top" withArrow>
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
{item.icon}
<IconLeaf size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{item.title}
{tujuan.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<List fz="h4" spacing="sm" withPadding>
{item.listDeskripsi.map((desc, idx) => (
<ListItem key={idx}>{desc}</ListItem>
))}
</List>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
))}
</Box>
{/* Materi Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{materi.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
</Box>
{/* Contoh Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={contoh.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconPlant2 size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{contoh.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Stack>

View File

@@ -75,7 +75,7 @@ export default function Content({ kategori }: { kategori: string }) {
{featured.kategoriKegiatan?.nama || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text color="dimmed" lineClamp={3} mb="md">{featured.deskripsiLengkap}</Text>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
@@ -135,9 +135,9 @@ export default function Content({ kategori }: { kategori: string }) {
{item.kategoriKegiatan?.nama || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" color="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" color="dimmed">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',

View File

@@ -1,4 +1,3 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./content";

View File

@@ -1,113 +1,391 @@
// 'use client'
// import colors from '@/con/colors';
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../../desa/layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsGotongRoyong({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// // Get active tab from URL path
// const activeTab = pathname.split('/').pop() || 'semua';
// // Get initial search value from URL
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// // Update active tab state when pathname changes
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// // Clean up timeouts on unmount
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// };
// }, [searchTimeout]);
// // Handle search input change with debounce
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// // Clear previous timeout
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// // Set new timeout
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) {
// params.set('search', value);
// } else {
// params.delete('search');
// }
// // Only update URL if the search value has actually changed
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
// }
// }, 500); // 500ms debounce delay
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// {
// label: "Semua",
// value: "semua",
// href: "/darmasaba/lingkungan/gotong-royong/semua"
// },
// {
// label: "Kebersihan",
// value: "kebersihan",
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
// },
// {
// label: "Infrastruktur",
// value: "infrastruktur",
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
// },
// {
// label: "Sosial",
// value: "sosial",
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
// },
// {
// label: "Lingkungan",
// value: "lingkungan",
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
// }
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Container size="lg" px="md">
// <Stack align="center" gap="0" >
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
// Gotong Royong Desa Darmasaba
// </Text>
// <Text ta="center" px="md">
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
// </Text>
// </Stack>
// </Container>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// <Grid>
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
// <TabsList>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </GridCol>
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </GridCol>
// </Grid>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsGotongRoyong;
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsBerita({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// const activeTab = pathname.split('/').pop() || 'semua';
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// };
// }, [searchTimeout]);
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) params.set('search', value);
// else params.delete('search');
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
// }
// }, 500);
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Box px={{ base: 'md', md: 100 }}>
// <Group justify='space-between' align="center">
// <Stack gap="0">
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
// Portal Berita Darmasaba
// </Text>
// <Text>
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
// </Text>
// </Stack>
// <Box>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </Box>
// </Group>
// </Box>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// {/* SCROLLABLE TABS */}
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// style={{
// flex: '0 0 auto', // Prevent shrinking
// minWidth: 100, // optional: makes them touch-friendly
// textAlign: 'center'
// }}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </Box>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsBerita;
'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
type HeaderSearchProps = {
placeholder?: string;
searchIcon?: React.ReactNode;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
children?: React.ReactNode;
};
function LayoutTabsGotongRoyong({
children,
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />
}: HeaderSearchProps) {
function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// Get active tab from URL path
const kategoriState = useProxy(gotongRoyongState.kategoriKegiatan);
// tab aktif dari url
const activeTab = pathname.split('/').pop() || 'semua';
// Get initial search value from URL
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Update active tab state when pathname changes
const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => {
kategoriState.findMany.load(); // ambil kategori dari DB
}, []);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
// Clean up timeouts on unmount
useEffect(() => {
return () => {
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
};
}, [searchTimeout]);
// search
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Handle search input change with debounce
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Clear previous timeout
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
// Set new timeout
if (searchTimeout !== null) clearTimeout(searchTimeout);
const newTimeout = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) {
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
}
}, 500); // 500ms debounce delay
if (value) params.set('search', value);
else params.delete('search');
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`);
}, 500);
setSearchTimeout(newTimeout);
};
// --- tabs dinamis ---
const tabs = [
{
label: "Semua",
value: "semua",
href: "/darmasaba/lingkungan/gotong-royong/semua"
},
{
label: "Kebersihan",
value: "kebersihan",
href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
},
{
label: "Infrastruktur",
value: "infrastruktur",
href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
},
{
label: "Sosial",
value: "sosial",
href: "/darmasaba/lingkungan/gotong-royong/sosial"
},
{
label: "Lingkungan",
value: "lingkungan",
href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
}
{ label: "Semua", value: "semua", href: "/darmasaba/lingkungan/gotong-royong/semua" },
...(kategoriState.findMany.data || []).map((kat: any) => ({
label: kat.nama,
value: kat.nama.toLowerCase(),
href: `/darmasaba/lingkungan/gotong-royong/${kat.nama.toLowerCase()}`
}))
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = tabs.find(t => t.value === value);
if (tab) {
const params = new URLSearchParams(searchParams.toString());
router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
router.push(`${tab.href}${params.toString() ? `?${params.toString()}` : ''}`);
}
};
@@ -117,17 +395,29 @@ function LayoutTabsGotongRoyong({
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Gotong Royong Desa Darmasaba
</Text>
<Text ta="center" px="md">
Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
<Group justify='space-between' align="center">
<Stack gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Portal Gotong royong Darmasaba
</Text>
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
</Stack>
<Box>
<TextInput
radius="lg"
placeholder="pencarian"
leftSection={<IconSearch size={20} />}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</Box>
</Group>
</Box>
{/* TABS */}
<Tabs
color={colors['blue-button']}
variant="pills"
@@ -135,31 +425,24 @@ function LayoutTabsGotongRoyong({
onChange={handleTabChange}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</GridCol>
</Grid>
<Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
<TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
style={{
flex: '0 0 auto',
minWidth: 100,
textAlign: 'center'
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
</Box>
{children}
@@ -168,4 +451,4 @@ function LayoutTabsGotongRoyong({
);
}
export default LayoutTabsGotongRoyong;
export default LayoutTabsGotongRoyong;

View File

@@ -5,7 +5,7 @@ import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridC
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
@@ -14,8 +14,7 @@ function Page() {
// Parameter URL
const search = searchParams.get('search') || '';
const currentPage = parseInt(searchParams.get('page') || '1');
const [page, setPage] = useState(currentPage);
const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state
const state = useProxy(gotongRoyongState.kegiatanDesa);
@@ -37,12 +36,14 @@ function Page() {
}, [page, search]);
// Update URL saat page berubah
useEffect(() => {
const url = new URLSearchParams();
const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search);
if (page > 1) url.set('page', page.toString());
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL
router.replace(`?${url.toString()}`);
}, [page, search]);
};
const featuredData = featured.data;
const paginatedNews = state.findMany.data || [];
@@ -77,9 +78,7 @@ function Page() {
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Title order={2} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md">
{featuredData.deskripsiSingkat}
</Text>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
@@ -146,7 +145,7 @@ function Page() {
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs">{item.deskripsiSingkat}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
@@ -169,7 +168,7 @@ function Page() {
<Pagination
total={totalPages}
value={page}
onChange={setPage}
onChange={handlePageChange}
siblings={1}
boundaries={1}
withEdges

View File

@@ -1,47 +1,30 @@
'use client'
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
import colors from '@/con/colors';
import { Box, Center, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
const data = [
{
id: 1,
title: 'Filosofi Tri Hita Karana',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Parahyangan: Hubungan manusia dengan Tuhan yang dijaga penuh kesadaran spiritual</ListItem>
<ListItem>Pawongan: Harmoni dan kerja sama antar manusia dalam masyarakat</ListItem>
<ListItem>Palemahan: Pelestarian lingkungan dan hubungan manusia dengan alam</ListItem>
</List>
),
},
{
id: 2,
title: 'Bentuk Konservasi Berdasarkan Adat',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Pelestarian Hutan Adat seperti Alas Pala Sangeh dan Wana Kerthi</ListItem>
<ListItem>Subak: Sistem irigasi tradisional yang menekankan kebersamaan dan keberlanjutan</ListItem>
<ListItem>Hari Raya Tumpek Uduh: Perayaan untuk menghormati pohon dan tumbuhan</ListItem>
<ListItem>Perarem & Awig-Awig: Aturan adat untuk menjaga lingkungan dari kerusakan</ListItem>
<ListItem>Ritual penyucian alam seperti Melasti dan Piodalan Segara</ListItem>
</List>
),
},
{
id: 3,
title: 'Nilai Konservasi Adat',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Menjaga keseimbangan ekosistem dan lingkungan hidup</ListItem>
<ListItem>Melestarikan spiritualitas lokal dan kesucian alam</ListItem>
<ListItem>Meningkatkan kesadaran kolektif untuk hidup selaras dengan alam</ListItem>
<ListItem>Menjamin keberlanjutan sumber daya alam untuk generasi mendatang</ListItem>
</List>
),
},
];
function Page() {
const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById)
const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById)
const bentuk = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat.findById)
useShallowEffect(() => {
filosofi.load('edit')
nilai.load('edit')
bentuk.load('edit')
}, [])
if (filosofi.loading || !filosofi.data || nilai.loading || !nilai.data || bentuk.loading || !bentuk.data) {
return (
<Stack py={20} align="center">
<Skeleton radius="md" height={600} width="100%" />
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="24">
<Box px={{ base: 'md', md: 100 }}>
@@ -56,24 +39,99 @@ function Page() {
</Text>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{data.map((item) => (
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
{/* Filsosofi */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
key={item.id}
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{ borderRadius: 16, boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)' }}
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20}>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{item.title}
</Text>
{filosofi.data?.judul}
</Text>
</Center>
{item.listDeskripsi}
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
))}
</Box>
{/* Nilai */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{nilai.data?.judul}
</Text>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
}}
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
{/* Bentuk */}
<Box>
<Paper
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{bentuk.data?.judul}
</Text>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
}}
dangerouslySetInnerHTML={{ __html: bentuk.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Stack>

View File

@@ -1,10 +1,10 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React from 'react';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import dynamic from 'next/dynamic';
@@ -20,20 +20,26 @@ function Page() {
const state = useProxy(pengelolaanSampahState.pengelolaanSampah)
const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500);
const {
data,
load
load,
} = state.findMany
const {
data: data2,
load: load2
load: load2,
page,
totalPages,
} = state2.findMany
useShallowEffect(() => {
load()
load2()
}, [])
load2(page, 3, debouncedSearch)
}, [page, debouncedSearch])
const iconMap: Record<string, Icon> = {
ekowisata: IconLeaf,
@@ -104,8 +110,10 @@ function Page() {
px={{ base: 70, md: 150 }}
leftSection={<IconSearch size={20} />}
placeholder='Cari Bank Sampah Terdekat'
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
{/* Left side - List of bank locations */}
<Box>
@@ -131,9 +139,17 @@ function Page() {
</Paper>
))}
</Stack>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</Paper>
</Box>
{/* Right side - Single map showing all locations */}
<Box style={{ position: 'sticky', top: '20px' }}>
<Paper p="md" bg={colors['white-trans-1']} radius="lg" h="100%">

View File

@@ -1,12 +1,13 @@
'use client'
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
const dataBeasiswa = [
{ id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
@@ -14,15 +15,11 @@ const dataBeasiswa = [
{ id: 3, nama: 'Dana Tersalurkan', jumlah: '1.5M', icon: IconCoin },
];
const dataProgram = [
{ id: 1, judul: "Pelatihan SoftSkill", deskripsi: "Pengembangan diri untuk mempersiapkan karir masa depan." },
{ id: 2, judul: "Peningkatan Akses Pendidikan", deskripsi: "Memberi kesempatan bagi masyarakat kurang mampu untuk tetap sekolah." },
{ id: 3, judul: "Pendampingan Intensif", deskripsi: "Bimbingan dari mentor berpengalaman untuk mendukung akademik." },
];
function Page() {
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
const ungggulanDesa = useProxy(beasiswaDesaState.keunggulanProgram)
const [opened, { open, close }] = useDisclosure(false);
const router = useTransitionRouter()
const resetForm = () => {
beasiswaDesa.create.form = {
namaLengkap: "",
@@ -41,6 +38,12 @@ function Page() {
};
};
const { data, page, totalPages, loading, load } = ungggulanDesa.findMany;
useShallowEffect(() => {
load(page, 3, "");
}, [page])
const handleSubmit = async () => {
await beasiswaDesa.create.create();
resetForm();
@@ -51,6 +54,14 @@ function Page() {
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={200} radius="md" />
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={40}>
<Box px={{ base: 'md', md: 100 }}>
@@ -70,13 +81,13 @@ function Page() {
<Button size="lg" radius="xl" bg={colors['blue-button']} rightSection={<IconArrowRight size={20} />} onClick={open}>
Daftar Sekarang
</Button>
<Button size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
<Button onClick={() => router.push('/darmasaba/pendidikan/beasiswa-desa/pelajari-lebih-lanjut')} size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
Pelajari Lebih Lanjut
</Button>
</Group>
</Box>
<Box>
<Image alt="Beasiswa Desa" src="/beasiswa-siswa.png" radius="lg" loading="lazy"/>
<Image alt="Beasiswa Desa" src="/beasiswa-siswa.png" radius="lg" loading="lazy" />
</Box>
</SimpleGrid>
@@ -101,14 +112,29 @@ function Page() {
Keunggulan Program
</Title>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{dataProgram.map((v, k) => (
{data.map((v, k) => (
<Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
<Text fz="sm" c="dimmed">{v.deskripsi}</Text>
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}/>
</Paper>
))}
</SimpleGrid>
<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>
<Title py={40} ta="center" order={1} fw={900} c={colors['blue-button']}>
Timeline Pendaftaran
</Title>
@@ -142,66 +168,66 @@ function Page() {
>
<Paper p="lg" radius="xl" withBorder shadow="sm">
<Stack gap="sm">
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
<TextInput
label="Tempat Lahir"
placeholder="Masukkan tempat lahir"
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
<TextInput
type="date"
label="Tanggal Lahir"
placeholder="Pilih tanggal lahir"
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
<TextInput
label="Kewarganegaraan"
placeholder="Masukkan kewarganegaraan"
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
<Select
label="Agama"
placeholder="Pilih agama"
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
<TextInput
label="Alamat KTP"
placeholder="Masukkan alamat sesuai KTP"
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
<TextInput
label="Alamat Domisili"
placeholder="Masukkan alamat domisili"
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
<TextInput
type="number"
label="Nomor HP"
placeholder="Masukkan nomor HP"
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
<TextInput
type="email"
label="Email"
placeholder="Masukkan alamat email"
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
<Select
label="Status Pernikahan"
placeholder="Pilih status pernikahan"
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
<Select
label="Ukuran Baju"
placeholder="Pilih ukuran baju"
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
<TextInput
label="Tempat Lahir"
placeholder="Masukkan tempat lahir"
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
<TextInput
type="date"
label="Tanggal Lahir"
placeholder="Pilih tanggal lahir"
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
<TextInput
label="Kewarganegaraan"
placeholder="Masukkan kewarganegaraan"
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
<Select
label="Agama"
placeholder="Pilih agama"
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
<TextInput
label="Alamat KTP"
placeholder="Masukkan alamat sesuai KTP"
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
<TextInput
label="Alamat Domisili"
placeholder="Masukkan alamat domisili"
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
<TextInput
type="number"
label="Nomor HP"
placeholder="Masukkan nomor HP"
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
<TextInput
type="email"
label="Email"
placeholder="Masukkan alamat email"
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
<Select
label="Status Pernikahan"
placeholder="Pilih status pernikahan"
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
<Select
label="Ukuran Baju"
placeholder="Pilih ukuran baju"
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
<Group justify="flex-end" mt="md">
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>

View File

@@ -0,0 +1,278 @@
'use client';
import {
Box,
Button,
Container,
Group,
Modal,
Paper,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
Timeline,
Title
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useDisclosure } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
export default function BeasiswaPage() {
const router = useRouter();
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
const [opened, { open, close }] = useDisclosure(false);
const resetForm = () => {
beasiswaDesa.create.form = {
namaLengkap: "",
nik: "",
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
agama: "",
alamatKTP: "",
alamatDomisili: "",
noHp: "",
email: "",
statusPernikahan: "",
ukuranBaju: "",
};
};
const handleSubmit = async () => {
await beasiswaDesa.create.create();
resetForm();
close();
};
return (
<Box bg="#f1f5fb" pb="xl" pt="md">
{/* Tombol Kembali */}
<Container size="lg">
<Button
variant="subtle"
color="blue"
leftSection={<IconArrowLeft size={18} />}
onClick={() => router.back()}
mb="lg"
>
Kembali
</Button>
</Container>
{/* Hero Section */}
<Container size="lg" py="xl">
<Stack gap="md" maw={600}>
<Title order={2} c="blue.9">
Program Beasiswa Pendidikan Desa Darmasaba
</Title>
<Text c="dimmed">
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
</Text>
</Stack>
</Container>
{/* Tentang Program */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Tentang Program
</Title>
<Text>
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
</Text>
</Container>
{/* Syarat dan Ketentuan */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Syarat Pendaftaran
</Title>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Domisili Desa Darmasaba</Text>
<Text c="dimmed" fz="sm">
Peserta harus merupakan warga desa yang berdomisili minimal 2 tahun.
</Text>
</Paper>
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Nilai Akademik</Text>
<Text c="dimmed" fz="sm">
Rata-rata nilai raport minimal 80 atau setara.
</Text>
</Paper>
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Surat Rekomendasi</Text>
<Text c="dimmed" fz="sm">
Diperlukan surat rekomendasi dari sekolah atau guru wali kelas.
</Text>
</Paper>
</SimpleGrid>
</Container>
{/* Proses Seleksi */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Proses Seleksi
</Title>
<Timeline active={4} bulletSize={24} lineWidth={2}>
<Timeline.Item title="Pendaftaran Online">
<Text c="dimmed" size="sm">
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text>
</Timeline.Item>
<Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas.
</Text>
</Timeline.Item>
<Timeline.Item title="Wawancara dan Penilaian">
<Text c="dimmed" size="sm">
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text>
</Timeline.Item>
<Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text>
</Timeline.Item>
</Timeline>
</Container>
{/* Testimoni */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Cerita Sukses Penerima Beasiswa
</Title>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg">
<Text fs={'italic'}>
Program ini sangat membantu saya melanjutkan kuliah di Universitas Udayana. Terima kasih Desa Darmasaba!
</Text>
<Text mt="sm" fw={600}>
Ni Kadek Ayu S., Penerima Beasiswa 2024
</Text>
</Paper>
<Paper shadow="md" p="lg" radius="lg">
<Text fs={'italic'}>
Selain bantuan dana, kami juga mendapatkan pelatihan komputer dan bahasa Inggris.
</Text>
<Text mt="sm" fw={600}>
I Made Gede A., Penerima Beasiswa 2023
</Text>
</Paper>
</SimpleGrid>
</Container>
{/* CTA Akhir */}
<Container size="lg" py="xl" ta="center">
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
<Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text>
<Button onClick={open} size="lg" radius="xl" color="blue">
Daftar Sekarang
</Button>
</Container>
<Modal
opened={opened}
onClose={close}
radius="xl"
size="lg"
transitionProps={{ transition: 'fade', duration: 200 }}
title={
<Text fz="xl" fw={800} c={colors['blue-button']}>
Formulir Beasiswa
</Text>
}
>
<Paper p="lg" radius="xl" withBorder shadow="sm">
<Stack gap="sm">
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
<TextInput
label="Tempat Lahir"
placeholder="Masukkan tempat lahir"
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
<TextInput
type="date"
label="Tanggal Lahir"
placeholder="Pilih tanggal lahir"
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
<TextInput
label="Kewarganegaraan"
placeholder="Masukkan kewarganegaraan"
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
<Select
label="Agama"
placeholder="Pilih agama"
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
<TextInput
label="Alamat KTP"
placeholder="Masukkan alamat sesuai KTP"
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
<TextInput
label="Alamat Domisili"
placeholder="Masukkan alamat domisili"
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
<TextInput
type="number"
label="Nomor HP"
placeholder="Masukkan nomor HP"
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
<TextInput
type="email"
label="Email"
placeholder="Masukkan alamat email"
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
<Select
label="Status Pernikahan"
placeholder="Pilih status pernikahan"
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
<Select
label="Ukuran Baju"
placeholder="Pilih ukuran baju"
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
<Group justify="flex-end" mt="md">
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
</Group>
</Stack>
</Paper>
</Modal>
</Box>
);
}

View File

@@ -51,7 +51,7 @@ function Page() {
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
Tujuan Program
{stateTujuanProgram.findById.data?.judul}
</Badge>
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
<Box>
@@ -66,7 +66,7 @@ function Page() {
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
Lokasi & Jadwal
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
<Box>
@@ -81,7 +81,7 @@ function Page() {
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
Fasilitas
{stateFasilitas.findById.data?.judul}
</Badge>
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
<Box>

View File

@@ -1,5 +1,101 @@
/* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from '@/con/colors';
// import {
// ActionIcon,
// Box,
// Button,
// Container,
// Group,
// Paper,
// Stack,
// Text,
// VisuallyHidden
// } from '@mantine/core';
// import { IconArrowLeft } from '@tabler/icons-react';
// import { useRouter, useSearchParams } from 'next/navigation';
// import React, { useState } from 'react';
// type LayoutSekolahProps = {
// title?: string;
// jenjangPendidikanList?: string[];
// children: React.ReactNode;
// };
// export default function LayoutSekolah({
// title = 'Cari Informasi Sekolah',
// jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
// children,
// }: LayoutSekolahProps) {
// const router = useRouter();
// const searchParams = useSearchParams();
// const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
// const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
// // Cleanup timeout
// // Handle jenjang pendidikan click
// const handleJenjangPendidikanChange = (k: string) => {
// // arahkan langsung ke route jenjang pendidikan
// if (k.toLowerCase() === 'semua') {
// setJenjangPendidikanAktif(k);
// router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
// } else {
// setJenjangPendidikanAktif(k);
// router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
// }
// };
// return (
// <Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
// <Container size="xl" py={{ base: 'md', md: 'xl' }}>
// <Stack gap="lg">
// {/* Back Button */}
// <ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
// <IconArrowLeft size={20} />
// <VisuallyHidden>Kembali</VisuallyHidden>
// </ActionIcon>
// {/* Search & Filter */}
// <Paper radius="lg" p="xl" withBorder>
// <Stack gap="md">
// <Text ta="center" fw={800} fz={28}>{title}</Text>
// <Text ta="center" fz={"md"} c="black">
// Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
// </Text>
// <Group justify="center" gap="xs" wrap="wrap">
// {jenjangPendidikanList.map((k) => {
// const aktif = k === jenjangPendidikanAktif;
// return (
// <Button
// key={k}
// onClick={() => handleJenjangPendidikanChange(k)}
// radius="xl"
// size="sm"
// variant={aktif ? 'filled' : 'light'}
// >
// {k}
// </Button>
// );
// })}
// </Group>
// </Stack>
// </Paper>
// {/* Slot konten */}
// {children}
// </Stack>
// </Container>
// </Box>
// );
// }
'use client'
import colors from '@/con/colors';
// pastikan path benar
import {
ActionIcon,
Box,
@@ -9,44 +105,48 @@ import {
Paper,
Stack,
Text,
VisuallyHidden
VisuallyHidden,
Loader,
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useState } from 'react';
import { useSnapshot } from 'valtio';
import React, { useEffect, useState } from 'react';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
type LayoutSekolahProps = {
title?: string;
jenjangPendidikanList?: string[];
children: React.ReactNode;
};
export default function LayoutSekolah({
title = 'Cari Informasi Sekolah',
jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
children,
}: LayoutSekolahProps) {
const router = useRouter();
const searchParams = useSearchParams();
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
const snap = useSnapshot(infoSekolahPaud.jenjangPendidikan);
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState<string>(
searchParams.get('jenjangPendidikan') || 'Semua'
);
// Cleanup timeout
// Load jenjang pendidikan dari backend
useEffect(() => {
if (!snap.findMany.data) infoSekolahPaud.jenjangPendidikan.findMany.load(1, 100);
}, []);
// Handle jenjang pendidikan click
const handleJenjangPendidikanChange = (k: string) => {
// arahkan langsung ke route jenjang pendidikan
if (k.toLowerCase() === 'semua') {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
} else {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
}
const handleJenjangPendidikanChange = (nama: string) => {
setJenjangPendidikanAktif(nama);
const path =
nama.toLowerCase() === 'semua'
? `/darmasaba/pendidikan/info-sekolah/semua`
: `/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(nama.toLowerCase())}`;
router.push(path);
};
// List tab dari data state
const jenjangList = ['Semua', ...(snap.findMany.data?.map((v) => v.nama) || [])];
return (
<Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
@@ -61,31 +161,41 @@ export default function LayoutSekolah({
{/* Search & Filter */}
<Paper radius="lg" p="xl" withBorder>
<Stack gap="md">
<Text ta="center" fw={800} fz={28}>{title}</Text>
<Text ta="center" fz={"md"} c="black">
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
<Text ta="center" fw={800} fz={28}>
{title}
</Text>
<Group justify="center" gap="xs" wrap="wrap">
{jenjangPendidikanList.map((k) => {
const aktif = k === jenjangPendidikanAktif;
return (
<Button
key={k}
onClick={() => handleJenjangPendidikanChange(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
>
{k}
</Button>
);
})}
</Group>
<Text ta="center" fz="md" c="black">
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga
pengajar berdasarkan jenjang pendidikan (TK, SD, SMP, SMA).
</Text>
{snap.findMany.loading ? (
<Group justify="center" py="sm">
<Loader size="sm" />
</Group>
) : (
<Group justify="center" gap="xs" wrap="wrap">
{jenjangList.map((k) => {
const aktif = k === jenjangPendidikanAktif;
return (
<Button
key={k}
onClick={() => handleJenjangPendidikanChange(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
>
{k}
</Button>
);
})}
</Group>
)}
</Stack>
</Paper>
{/* Slot konten */}
{/* Konten anak */}
{children}
</Stack>
</Container>

View File

@@ -62,7 +62,7 @@ function Page() {
<Tooltip label="Fokus utama program" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconTarget size={28} style={{ marginRight: 8 }} />
Tujuan Program
{stateTujuanPendidikanNonFormal.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
@@ -79,7 +79,7 @@ function Page() {
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconMapPin size={28} style={{ marginRight: 8 }} />
Tempat Kegiatan
{stateTempatKegiatan.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
@@ -98,7 +98,7 @@ function Page() {
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconBook2 size={28} style={{ marginRight: 8 }} />
Jenis Program yang Diselenggarakan
{stateJenisProgram.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />

View File

@@ -0,0 +1,126 @@
'use client';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Image,
Loader,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowLeft, IconBook2 } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import ModalPeminjaman from '../../_lib/modalPeminjaman';
export default function DetailBukuUser() {
const router = useRouter();
const params = useParams();
const stateDetail = useProxy(perpustakaanDigitalState.dataPerpustakaan);
const [opened, setOpened] = useState(false);
useShallowEffect(() => {
if (params?.id) stateDetail.findUnique.load(params.id as string);
}, [params?.id]);
const data = stateDetail.findUnique.data;
if (!data) {
return (
<Center h="70vh">
<Loader color={colors['blue-button']} size="lg" />
</Center>
);
}
return (
<Box bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }}>
{/* Tombol Kembali */}
<Button
variant="subtle"
leftSection={<IconArrowLeft size={20} color={colors['blue-button']} />}
onClick={() => router.back()}
mb="lg"
>
Kembali
</Button>
<Paper
withBorder
shadow="md"
radius="xl"
bg="white"
p={{ base: 'md', md: 'xl' }}
maw={800}
mx="auto"
>
<Stack gap="lg" align="center">
{/* Cover Buku */}
<Image
src={data.image?.link || '/placeholder-book.jpg'}
alt={data.judul}
w={220}
h={300}
fit="contain"
radius="md"
loading="lazy"
/>
{/* Judul & Kategori */}
<Stack gap={4} align="center">
<Title order={2} ta="center" c={colors['blue-button']}>
{data.judul}
</Title>
{data.kategori?.name && (
<Badge color="cyan" variant="light" size="md">
{data.kategori.name}
</Badge>
)}
</Stack>
{/* Deskripsi Buku */}
<Paper bg="#F9FAFF" p="md" radius="md" shadow="xs" w="100%">
<Text fw={600} mb={6} c={colors['blue-button']}>
Deskripsi Buku
</Text>
<Text
fz="sm"
lh={1.6}
ta="justify"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Paper>
{/* Tombol Pinjam */}
<Button
size="md"
radius="xl"
color="blue"
mt="sm"
leftSection={<IconBook2 size={20} />}
onClick={() => setOpened(true)}
>
Pinjam Buku Ini
</Button>
</Stack>
</Paper>
{/* Modal Peminjaman */}
<ModalPeminjaman
opened={opened}
onClose={() => setOpened(false)}
buku={data}
/>
</Box>
);
}

View File

@@ -1,42 +1,52 @@
'use client'
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import { ActionIcon, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip, Badge } from '@mantine/core';
import { ActionIcon, Badge, Box, Button, Center, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBook2, IconRefresh } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
import { useSearchParams } from 'next/navigation';
import { useCallback, useState } from 'react';
import { useProxy } from 'valtio/utils';
function Content({ kategoriBuku }: { kategoriBuku: string }) {
const state = useProxy(perpustakaanDigitalState);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const searchParams = useSearchParams();
const searchQuery = searchParams.get('search') || '';
const router = useTransitionRouter()
const decodedKategoriBuku = decodeURIComponent(kategoriBuku);
const kategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
const loadData = useCallback(async (searchQuery: string = '') => {
const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => {
try {
setIsLoading(true);
await state.dataPerpustakaan.findMany.load(1, 100, searchQuery, kategoriFilter);
const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter);
setCurrentPage(page);
setTotalPages(state.dataPerpustakaan.findMany.totalPages);
} finally {
setIsLoading(false);
}
}, [kategoriFilter, state.dataPerpustakaan.findMany]);
}, [state.dataPerpustakaan.findMany, decodedKategoriBuku]);
useShallowEffect(() => {
loadData(searchQuery);
}, [searchQuery, loadData]);
}, [searchQuery, loadData, kategoriBuku]);
const handleRefresh = () => {
loadData();
};
if (isLoading || !state.dataPerpustakaan.findMany.load || !state.dataPerpustakaan.findMany.data) {
const handlePageChange = (newPage: number) => {
loadData(searchQuery, newPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
if ((isLoading && !state.dataPerpustakaan.findMany.data) || !state.dataPerpustakaan.findMany.load) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={50}>
@@ -57,18 +67,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
Koleksi Buku
</Text>
</Group>
<Tooltip label="Muat ulang koleksi" withArrow>
<ActionIcon
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
onClick={handleRefresh}
loading={isLoading}
radius="xl"
size="lg"
>
<IconRefresh size={20} />
</ActionIcon>
</Tooltip>
<Group>
<Text size="sm" c="dimmed">
Halaman {currentPage} dari {totalPages}
</Text>
<Tooltip label="Muat ulang koleksi" withArrow>
<ActionIcon
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
onClick={handleRefresh}
loading={isLoading}
radius="xl"
size="lg"
>
<IconRefresh size={20} />
</ActionIcon>
</Tooltip>
</Group>
</Group>
{!state.dataPerpustakaan.findMany.data || state.dataPerpustakaan.findMany.data.length === 0 ? (
@@ -132,30 +147,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
</Badge>
)}
</Stack>
<Spoiler
maxHeight={80}
showLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Lihat deskripsi
</Text>
}
hideLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Sembunyikan deskripsi
</Text>
}
expanded={expandedId === v.id}
onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)}
>
<Text
ta="justify"
fz="sm"
lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Spoiler>
<Text
ta="justify"
fz="sm"
lh={1.5}
lineClamp={5}
truncate="end"
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
/>
{/* 📗 Tombol Detail */}
<Button variant='light' color='blue' onClick={() => router.push(`/darmasaba/pendidikan/perpustakaan-digital/${v.kategori?.name}/${v.id}`)}>
Lihat Detail
</Button>
</Stack>
</Paper>
</motion.div>
@@ -163,6 +171,15 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
</SimpleGrid>
)}
</Box>
<Center mt="lg">
<Pagination
value={currentPage}
onChange={handlePageChange}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
</Stack>
);
}

View File

@@ -1,11 +1,24 @@
'use client'
import { useEffect, useState } from 'react';
import { ActionIcon, Box, Flex, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch, IconUser } from '@tabler/icons-react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import BackButton from '../../../desa/layanan/_com/BackButto';
'use client';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Box,
Grid,
GridCol,
Stack,
Tabs,
TabsList,
TabsTab,
Text,
TextInput
} from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import BackButton from '../../../desa/layanan/_com/BackButto';
type LayoutBukuProps = {
placeholder?: string;
@@ -15,7 +28,7 @@ type LayoutBukuProps = {
children?: React.ReactNode;
};
function LayoutTabs({
export default function LayoutTabs({
placeholder = 'Cari buku digital...',
searchIcon = <IconSearch size={20} />,
children,
@@ -23,6 +36,7 @@ function LayoutTabs({
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const snap = useSnapshot(perpustakaanDigitalState);
const activeTab = pathname.split('/').pop() || 'semua';
const initialSearch = searchParams.get('search') || '';
@@ -30,6 +44,11 @@ function LayoutTabs({
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
const [activeTabState, setActiveTabState] = useState(activeTab);
// 🟦 Ambil kategori buku saat mount
useEffect(() => {
perpustakaanDigitalState.kategoriBuku.findMany.load();
}, []);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
@@ -51,7 +70,9 @@ function LayoutTabs({
if (value) params.set('search', value);
router.push(
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${
params.toString() ? `?${params.toString()}` : ''
}`
);
};
@@ -63,41 +84,40 @@ function LayoutTabs({
}
};
// 🟩 Tabs dinamis berdasarkan kategori dari state
const kategoriTabs =
snap.kategoriBuku.findMany.data?.map((item) => ({
label: item.name,
value: item.name.toLowerCase().replace(/\s+/g, '-'),
href: `/darmasaba/pendidikan/perpustakaan-digital/${encodeURIComponent(item.name.toLowerCase().replace(/\s+/g, '-'))}`,
})) ?? [];
const tabs = [
{ label: 'Semua', value: 'semua', href: '/darmasaba/pendidikan/perpustakaan-digital/semua' },
{ label: 'Dokumenter', value: 'dokumenter', href: '/darmasaba/pendidikan/perpustakaan-digital/dokumenter' },
{ label: 'Sayuran', value: 'sayuran', href: '/darmasaba/pendidikan/perpustakaan-digital/sayuran' },
{ label: 'Dongeng', value: 'dongeng', href: '/darmasaba/pendidikan/perpustakaan-digital/dongeng' },
...kategoriTabs,
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const params = new URLSearchParams(searchParams.toString());
router.push(`/darmasaba/pendidikan/perpustakaan-digital/${value}${params.toString() ? `?${params.toString()}` : ''}`);
router.push(
`/darmasaba/pendidikan/perpustakaan-digital/${value}${
params.toString() ? `?${params.toString()}` : ''
}`
);
};
return (
<Stack pos="relative" bg="var(--mantine-color-gray-0)" py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center">
<BackButton />
<ActionIcon
variant="light"
component={Link}
href="/login"
radius="xl"
size="lg"
aria-label="Masuk ke akun"
>
<IconUser size={26} stroke={1.5} />
</ActionIcon>
</Flex>
</Box>
<Box pb={20}>
<Text ta="center" fz={{ base: '1.6rem', md: '2.4rem' }} fw={700} c={colors['blue-button']}>
Perpustakaan Digital Darmasaba
</Text>
<Tabs color="blue" variant="pills" value={activeTabState} onChange={handleTabChange}>
<Box px={{ base: 'md', md: 100 }} py="md" bg="var(--mantine-color-gray-1)" style={{ borderRadius: 16 }}>
<Grid align="center" gutter="md">
@@ -116,6 +136,7 @@ function LayoutTabs({
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius="xl"
@@ -130,11 +151,10 @@ function LayoutTabs({
</GridCol>
</Grid>
</Box>
{children}
</Tabs>
</Box>
</Stack>
);
}
export default LayoutTabs;

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