nico/27-okt-25 #1
1
auth
1
auth
Submodule auth deleted from 51d749567a
@@ -49,6 +49,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
@@ -67,12 +68,15 @@
|
|||||||
"next-view-transitions": "^0.3.4",
|
"next-view-transitions": "^0.3.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primereact": "^10.9.6",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
|
|||||||
91
prisma/data/ppid/struktur-ppid/pegawai-PPID.json
Normal file
91
prisma/data/ppid/struktur-ppid/pegawai-PPID.json
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440006",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440017",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440018",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440021",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
158
prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
Normal file
158
prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id" : "1",
|
|
||||||
"name" : "Struktur PPID"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
6
prisma/data/user/role.json
Normal file
6
prisma/data/user/role.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cmdpm429r0000vnndkcwslt0h",
|
||||||
|
"name": "warga"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -98,6 +98,8 @@ model FileStorage {
|
|||||||
PrestasiDesa PrestasiDesa[]
|
PrestasiDesa PrestasiDesa[]
|
||||||
|
|
||||||
DataPerpustakaan DataPerpustakaan[]
|
DataPerpustakaan DataPerpustakaan[]
|
||||||
|
|
||||||
|
PegawaiPPID PegawaiPPID[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -296,14 +298,61 @@ model ResponseStat {
|
|||||||
|
|
||||||
//========================================= STRUKTUR PPID ========================================= //
|
//========================================= STRUKTUR PPID ========================================= //
|
||||||
model StrukturPPID {
|
model StrukturPPID {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @db.Text
|
name String @db.Text
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
PosisiOrganisasiPPID PosisiOrganisasiPPID? @relation(fields: [posisiOrganisasiPPIDId], references: [id])
|
||||||
|
posisiOrganisasiPPIDId String?
|
||||||
|
PegawaiPPID PegawaiPPID? @relation(fields: [pegawaiPPIDId], references: [id])
|
||||||
|
pegawaiPPIDId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= VISI MISI PPID ========================================= //
|
// ========================================= VISI MISI PPID ========================================= //
|
||||||
@@ -1316,8 +1365,9 @@ model PosisiOrganisasi {
|
|||||||
deskripsi String? @db.Text
|
deskripsi String? @db.Text
|
||||||
hierarki Int
|
hierarki Int
|
||||||
|
|
||||||
pegawai Pegawai[]
|
pegawai Pegawai[]
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
|
|
||||||
@@map("posisi_organisasi")
|
@@map("posisi_organisasi")
|
||||||
}
|
}
|
||||||
@@ -1342,7 +1392,8 @@ model Pegawai {
|
|||||||
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
|
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
|
||||||
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
|
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
|
||||||
|
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
|
|
||||||
@@map("pegawai")
|
@@map("pegawai")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import potensi from "./data/list-potensi.json";
|
|||||||
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
|
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
|
||||||
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
|
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
|
||||||
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
||||||
import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
|
|
||||||
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
||||||
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
||||||
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
||||||
@@ -37,6 +36,8 @@ import lokasiJadwalBimbinganBelajarDesa from './data/pendidikan/bimbingan-belaja
|
|||||||
import fasilitasBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json';
|
import fasilitasBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json';
|
||||||
import tempatKegiatan from './data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json';
|
import tempatKegiatan from './data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json';
|
||||||
import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json';
|
import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json';
|
||||||
|
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
|
||||||
|
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
|
||||||
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -154,6 +155,64 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
|
|
||||||
console.log("visi misi desa success ...");
|
console.log("visi misi desa success ...");
|
||||||
|
|
||||||
|
// Flatten the nested array structure for posisiOrganisasiPPID
|
||||||
|
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat();
|
||||||
|
|
||||||
|
for (const p of flattenedPosisiOrganisasiPPID) {
|
||||||
|
await prisma.posisiOrganisasiPPID.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: p.nama,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
hierarki: p.hierarki,
|
||||||
|
parentId: p.parentId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
nama: p.nama,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
hierarki: p.hierarki,
|
||||||
|
parentId: p.parentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("posisi organisasi success ...");
|
||||||
|
|
||||||
|
// Flatten the nested array structure for pegawaiPPID
|
||||||
|
const flattenedPegawaiPPID = pegawaiPPID.flat();
|
||||||
|
|
||||||
|
for (const p of flattenedPegawaiPPID) {
|
||||||
|
await prisma.pegawaiPPID.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
namaLengkap: p.namaLengkap,
|
||||||
|
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||||
|
email: p.email,
|
||||||
|
gelarAkademik: p.gelarAkademik,
|
||||||
|
telepon: p.telepon,
|
||||||
|
alamat: p.alamat,
|
||||||
|
posisiId: p.posisiId,
|
||||||
|
isActive: p.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
namaLengkap: p.namaLengkap,
|
||||||
|
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||||
|
email: p.email,
|
||||||
|
gelarAkademik: p.gelarAkademik,
|
||||||
|
telepon: p.telepon,
|
||||||
|
alamat: p.alamat,
|
||||||
|
posisiId: p.posisiId,
|
||||||
|
isActive: p.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("pegawai success ...");
|
||||||
|
|
||||||
for (const l of pelayananPerizinanBerusaha) {
|
for (const l of pelayananPerizinanBerusaha) {
|
||||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -193,21 +252,6 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
}
|
}
|
||||||
console.log("pelayanan penduduk non permanen success ...");
|
console.log("pelayanan penduduk non permanen success ...");
|
||||||
|
|
||||||
for (const s of strukturPPID) {
|
|
||||||
await prisma.strukturPPID.upsert({
|
|
||||||
where: {
|
|
||||||
id: s.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: s.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: s.id,
|
|
||||||
name: s.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("struktur ppid success ...");
|
|
||||||
|
|
||||||
for (const p of potensi) {
|
for (const p of potensi) {
|
||||||
await prisma.potensi.upsert({
|
await prisma.potensi.upsert({
|
||||||
|
|||||||
@@ -1,169 +1,683 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
})
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
name: "",
|
name: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
|
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
id: true;
|
id: true;
|
||||||
name: true;
|
name: true;
|
||||||
imageId: true;
|
imageId: true;
|
||||||
image?: {
|
image?: {
|
||||||
select: {
|
select: {
|
||||||
link: true;
|
link: true;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const stateStrukturPPID = proxy({
|
const stateStruktur = proxy({
|
||||||
struktur: {
|
struktur: {
|
||||||
data: null as StrukturPPIDForm | null,
|
data: null as StrukturPPIDForm | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid")
|
toast.warn("ID tidak valid");
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ppid/strukturppid/${id}`);
|
const response = await fetch(`/api/ppid/strukturppid/${id}`);
|
||||||
|
|
||||||
if(!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
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: {
|
const result = await response.json();
|
||||||
id: "",
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
error: null as string | null,
|
|
||||||
isReadOnly: false,
|
|
||||||
|
|
||||||
initialize(strukturData: StrukturPPIDForm) {
|
if (result.success) {
|
||||||
this.id = strukturData.id;
|
this.data = result.data;
|
||||||
this.isReadOnly = false;
|
return result.data;
|
||||||
this.form = {
|
} else {
|
||||||
name: strukturData.name || "",
|
throw new Error(result.message || "Gagal mengambil data struktur");
|
||||||
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/ppid/strukturppid/${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 stateStrukturPPID.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;
|
|
||||||
}
|
}
|
||||||
},
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
async loadForEdit(id: string) {
|
this.error = errorMessage;
|
||||||
const strukturData = await this.struktur.load(id);
|
console.error("Load struktur error:", errorMessage);
|
||||||
if (strukturData) {
|
toast.error("Terjadi kesalahan saat mengambil data struktur");
|
||||||
this.editStruktur.initialize(strukturData);
|
return null;
|
||||||
}
|
} finally {
|
||||||
return strukturData;
|
this.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.struktur.reset();
|
this.data = null;
|
||||||
this.editStruktur.reset();
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
editStruktur: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(strukturData: StrukturPPIDForm) {
|
||||||
|
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/ppid/strukturppid/${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"),
|
||||||
|
deskripsi: z.string().optional(),
|
||||||
|
hierarki: z.number().int().positive("Hierarki harus angka positif"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const posisiOrganisasiDefaultForm = {
|
||||||
|
nama: "",
|
||||||
|
deskripsi: "",
|
||||||
|
hierarki: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const posisiOrganisasi = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...posisiOrganisasiDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = templatePosisiOrganisasi.safeParse(this.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = cek.error.issues.map((v) => v.message).join("\n");
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
||||||
|
"create"
|
||||||
|
].post(this.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success("Berhasil menambahkan posisi organisasi");
|
||||||
|
posisiOrganisasi.findMany.load();
|
||||||
|
this.reset();
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || "Gagal menambahkan posisi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Create error:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menambahkan posisi");
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = { ...posisiOrganisasiDefaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.StrukturOrganisasiPPIDGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/ppid/strukturppid/posisiorganisasi/${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 },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/ppid/strukturppid/posisiorganisasi/${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,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
hierarki: data.hierarki,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading posisi organisasi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = templatePosisiOrganisasi.safeParse(this.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/ppid/strukturppid/posisiorganisasi/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
nama: this.form.nama,
|
||||||
|
deskripsi: this.form.deskripsi,
|
||||||
|
hierarki: this.form.hierarki,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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 posisi organisasi");
|
||||||
|
await posisiOrganisasi.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengupdate posisi organisasi"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating posisi organisasi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Gagal mengupdate posisi organisasi"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...posisiOrganisasiDefaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
findMany: {
|
||||||
|
data: [] as Array<{
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
deskripsi: string | null;
|
||||||
|
hierarki: number;
|
||||||
|
}>,
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
// The API now returns the id field, so we can use it directly
|
||||||
|
this.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find many error:", error);
|
||||||
|
this.data = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
posisiOrganisasi.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/ppid/strukturppid/posisiorganisasi/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Posisi organisasi berhasil dihapus");
|
||||||
|
await posisiOrganisasi.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus posisi organisasi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const templatePegawai = z.object({
|
||||||
|
namaLengkap: z.string().min(1, "Nama wajib diisi"),
|
||||||
|
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().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),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pegawaiDefaultForm = {
|
||||||
|
namaLengkap: "",
|
||||||
|
gelarAkademik: "",
|
||||||
|
imageId: "",
|
||||||
|
tanggalMasuk: "",
|
||||||
|
email: "",
|
||||||
|
telepon: "",
|
||||||
|
alamat: "",
|
||||||
|
posisiId: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pegawai = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...pegawaiDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = templatePegawai.safeParse(pegawai.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = cek.error.issues.map((i) => i.message).join("\n");
|
||||||
|
toast.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pegawai.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
|
"create"
|
||||||
|
].post(pegawai.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success("Pegawai berhasil ditambahkan");
|
||||||
|
await pegawai.findMany.load();
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message ?? "Gagal tambah pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal create:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
||||||
|
} finally {
|
||||||
|
pegawai.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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.ppid.strukturppid.pegawai[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as
|
||||||
|
| (Prisma.PegawaiGetPayload<{
|
||||||
|
include: { posisi: true; image: true };
|
||||||
|
}> & { isActive: boolean })
|
||||||
|
| null,
|
||||||
|
async load(id: string) {
|
||||||
|
const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
pegawai.findUnique.data = json.data
|
||||||
|
? {
|
||||||
|
...json.data,
|
||||||
|
isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
pegawai.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
try {
|
||||||
|
pegawai.delete.loading = true;
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/ppid/strukturppid/pegawai/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(json.message ?? "Berhasil hapus pegawai");
|
||||||
|
await pegawai.findMany.load();
|
||||||
|
} else {
|
||||||
|
toast.error(json.message ?? "Gagal hapus pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus");
|
||||||
|
} finally {
|
||||||
|
pegawai.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: {
|
||||||
|
id: "",
|
||||||
|
form: { ...pegawaiDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/ppid/strukturppid/pegawai/${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 = {
|
||||||
|
namaLengkap: data.namaLengkap,
|
||||||
|
gelarAkademik: data.gelarAkademik,
|
||||||
|
imageId: data.imageId,
|
||||||
|
tanggalMasuk: data.tanggalMasuk,
|
||||||
|
email: data.email,
|
||||||
|
telepon: data.telepon,
|
||||||
|
alamat: data.alamat,
|
||||||
|
posisiId: data.posisiId,
|
||||||
|
isActive: data.isActive,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading berita:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const cek = templatePegawai.safeParse(pegawai.edit.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pegawai.edit.loading = true;
|
||||||
|
|
||||||
|
// Format tanggalMasuk to ISO string if it exists
|
||||||
|
const formattedTanggalMasuk = this.form.tanggalMasuk
|
||||||
|
? new Date(this.form.tanggalMasuk).toISOString()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/ppid/strukturppid/pegawai/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: this.id,
|
||||||
|
namaLengkap: this.form.namaLengkap,
|
||||||
|
gelarAkademik: this.form.gelarAkademik,
|
||||||
|
imageId: this.form.imageId || null,
|
||||||
|
tanggalMasuk: formattedTanggalMasuk,
|
||||||
|
email: this.form.email,
|
||||||
|
telepon: this.form.telepon,
|
||||||
|
alamat: this.form.alamat,
|
||||||
|
posisiId: this.form.posisiId,
|
||||||
|
isActive: this.form.isActive,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 pegawai");
|
||||||
|
await pegawai.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating pegawai:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update pegawai"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
pegawai.edit.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
pegawai.edit.id = "";
|
||||||
|
pegawai.edit.form = { ...pegawaiDefaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateStrukturPPID = proxy({
|
||||||
|
stateStruktur,
|
||||||
|
posisiOrganisasi,
|
||||||
|
pegawai
|
||||||
|
});
|
||||||
|
|
||||||
export default stateStrukturPPID;
|
export default stateStrukturPPID;
|
||||||
|
|
||||||
@@ -1,220 +1,456 @@
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from 'valtio'
|
||||||
import { toast } from "react-toastify";
|
import { toast } from 'react-toastify'
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from '@/lib/api-fetch'
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from '@prisma/client'
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// 1. Validasi Zod
|
||||||
const userSchema = z.object({
|
const userSchema = z.object({
|
||||||
nama: z.string().min(1, "Nama harus diisi"),
|
nama: z.string().min(1, 'Nama harus diisi'),
|
||||||
email: z.string().email("Email tidak valid"),
|
email: z.string().email('Email tidak valid'),
|
||||||
password: z.string().min(6, "Password minimal 6 karakter"),
|
password: z.string().min(6, 'Password minimal 6 karakter'),
|
||||||
roleId: z.string().optional(),
|
roleId: z.string().optional(),
|
||||||
});
|
})
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
|
||||||
nama: "",
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
roleId: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 2. State Valtio
|
||||||
const userState = proxy({
|
const userState = proxy({
|
||||||
create: {
|
// Register
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
async create(isAdmin: boolean = false) {
|
|
||||||
const valid = userSchema.safeParse(userState.create.form);
|
|
||||||
if (!valid.success) {
|
|
||||||
const err = valid.error.issues.map((i) => i.message).join(", ");
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
userState.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.user[
|
|
||||||
isAdmin ? "create" : "register"
|
|
||||||
].post(userState.create.form);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success("User berhasil dibuat");
|
|
||||||
userState.findMany.load();
|
|
||||||
} else {
|
|
||||||
toast.error(res.data?.message || "Gagal membuat user");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast.error("Terjadi kesalahan saat membuat user");
|
|
||||||
} finally {
|
|
||||||
userState.create.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
login: {
|
|
||||||
form: { email: "", password: "" },
|
|
||||||
loading: false,
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
userState.login.loading = true;
|
|
||||||
const res = await ApiFetch.api.user.login.post(userState.login.form);
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success("Login berhasil");
|
|
||||||
const token = res.data?.data?.token;
|
|
||||||
if (typeof token === "string") {
|
|
||||||
localStorage.setItem("token", token);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(res.data?.message || "Login gagal");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast.error("Terjadi kesalahan saat login");
|
|
||||||
} finally {
|
|
||||||
userState.login.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
register: {
|
register: {
|
||||||
form: { ...defaultForm },
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit() {
|
async submit() {
|
||||||
const valid = userSchema.safeParse(userState.register.form);
|
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
|
||||||
if (!valid.success) {
|
if (!valid.success) {
|
||||||
const err = valid.error.issues.map(i => i.message).join(", ");
|
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||||
return toast.error(err);
|
return toast.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
userState.register.loading = true;
|
userState.register.loading = true
|
||||||
const res = await ApiFetch.api.user.register.post(userState.register.form);
|
const res = await ApiFetch.api.user.register.post(userState.register.form)
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Registrasi berhasil, silakan login");
|
toast.success('Registrasi berhasil, silakan login')
|
||||||
userState.register.form = { ...defaultForm }; // Reset form
|
userState.register.form = { ...defaultForm } // reset
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.data?.message || "Gagal registrasi");
|
toast.error(res.data?.message || 'Gagal registrasi')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
toast.error("Terjadi kesalahan saat registrasi");
|
toast.error('Terjadi kesalahan saat registrasi')
|
||||||
} finally {
|
} finally {
|
||||||
userState.register.loading = false;
|
userState.register.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Login
|
||||||
|
login: {
|
||||||
|
form: { email: '', password: '' },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
userState.login.loading = true
|
||||||
|
const res = await ApiFetch.api.user.login.post(userState.login.form)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Login berhasil')
|
||||||
|
const token = res.data?.data?.token
|
||||||
|
if (typeof token === 'string') {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
// Optional: simpan user role untuk otorisasi
|
||||||
|
const user = res.data?.data?.user
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || 'Login gagal')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Terjadi kesalahan saat login')
|
||||||
|
} finally {
|
||||||
|
userState.login.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// CRUD User (untuk admin)
|
||||||
|
create: {
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create(isAdmin = false) {
|
||||||
|
const valid = userSchema.safeParse(userState.create.form)
|
||||||
|
if (!valid.success) {
|
||||||
|
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||||
|
return toast.error(err)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
userState.create.loading = true
|
||||||
|
const endpoint = isAdmin ? 'create' : 'register'
|
||||||
|
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('User berhasil dibuat')
|
||||||
|
userState.findMany.load() // refresh list
|
||||||
|
userState.create.form = { ...defaultForm } // reset form
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || 'Gagal membuat user')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal membuat user')
|
||||||
|
} finally {
|
||||||
|
userState.create.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find Many
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
async load() {
|
||||||
userState.findMany.loading = true;
|
this.loading = true
|
||||||
const res = await ApiFetch.api.user.findMany.get();
|
try {
|
||||||
if (res.status === 200) {
|
const res = await ApiFetch.api.user.findMany.get()
|
||||||
userState.findMany.data = res.data?.data ?? [];
|
if (res.status === 200) {
|
||||||
|
this.data = res.data?.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal muat data user')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
}
|
}
|
||||||
userState.findMany.loading = false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Find Unique
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
userState.findUnique.loading = true;
|
const res = await fetch(`/api/user/findUnique/${id}`)
|
||||||
const res = await fetch(`/api/user/findUnique/${id}`);
|
const data = await res.json()
|
||||||
const data = await res.json();
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
userState.findUnique.data = data.data ?? null;
|
this.data = data.data
|
||||||
|
} else {
|
||||||
|
toast.error(data.message)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
toast.error("Terjadi kesalahan saat mengambil data user");
|
toast.error('Gagal ambil data user')
|
||||||
} finally {
|
} finally {
|
||||||
userState.findUnique.loading = false;
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: '',
|
||||||
form: { ...defaultForm },
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
userState.update.loading = true;
|
const res = await fetch(`/api/user/findUnique/${id}`)
|
||||||
const res = await fetch(`/api/user/findUnique/${id}`);
|
const data = await res.json()
|
||||||
const data = await res.json();
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const user = data.data;
|
const user = data.data
|
||||||
userState.update.id = user.id;
|
this.id = user.id
|
||||||
userState.update.form = {
|
this.form = {
|
||||||
nama: user.nama,
|
nama: user.nama,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
password: "",
|
password: '', // jangan kirim password lama
|
||||||
roleId: user.roleId,
|
roleId: user.roleId,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
toast.error("Terjadi kesalahan saat mengambil data user");
|
toast.error('Gagal muat data')
|
||||||
} finally {
|
} finally {
|
||||||
userState.update.loading = false;
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
userState.update.loading = true;
|
const res = await fetch(`/api/user/update/${this.id}`, {
|
||||||
const res = await fetch(`/api/user/update/${userState.update.id}`, {
|
method: 'PUT',
|
||||||
method: "PUT",
|
headers: { 'Content-Type': 'application/json' },
|
||||||
headers: {
|
body: JSON.stringify(this.form),
|
||||||
"Content-Type": "application/json",
|
})
|
||||||
},
|
const data = await res.json()
|
||||||
body: JSON.stringify(userState.update.form),
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Berhasil update user");
|
toast.success('Update berhasil')
|
||||||
userState.findMany.load();
|
userState.findMany.load()
|
||||||
} else {
|
} else {
|
||||||
toast.error(data?.message || "Gagal update user");
|
toast.error(data.message || 'Gagal update')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
toast.error("Terjadi kesalahan saat update user");
|
toast.error('Gagal update user')
|
||||||
} finally {
|
} finally {
|
||||||
userState.update.loading = false;
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete (Soft Delete)
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async submit(id: string) {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/user/del/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('User dinonaktifkan')
|
||||||
|
userState.findMany.load()
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal hapus')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal hapus user')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const templateRole = z.object({
|
||||||
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultRole = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleState = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultRole },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateRole.safeParse(roleState.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleState.create.loading = true;
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.role[
|
||||||
|
"create"
|
||||||
|
].post(roleState.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
roleState.findMany.load();
|
||||||
|
return toast.success("Data role Berhasil Dibuat");
|
||||||
|
}
|
||||||
|
console.log(res);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} finally {
|
||||||
|
roleState.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: [] as Prisma.RoleGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.role[
|
||||||
|
"findMany"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
roleState.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.RoleGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/role/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
roleState.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
roleState.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
roleState.findUnique.data = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string) {
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
userState.delete.loading = true;
|
roleState.delete.loading = true;
|
||||||
const res = await fetch(`/api/user/del/${id}`, {
|
|
||||||
method: "PUT",
|
const response = await fetch(
|
||||||
headers: {
|
`/api/role/del/${id}`,
|
||||||
"Content-Type": "application/json",
|
{
|
||||||
},
|
method: "DELETE",
|
||||||
body: JSON.stringify({}),
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
const data = await res.json();
|
},
|
||||||
if (res.status === 200) {
|
}
|
||||||
toast.success("User berhasil dihapus");
|
);
|
||||||
userState.findMany.load();
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message || "Data role berhasil dihapus"
|
||||||
|
);
|
||||||
|
await roleState.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(data?.message || "Gagal hapus user");
|
toast.error(result?.message || "Gagal menghapus Data role");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(e);
|
console.error("Gagal delete:", error);
|
||||||
toast.error("Terjadi kesalahan saat hapus user");
|
toast.error("Terjadi kesalahan saat menghapus Data role");
|
||||||
} finally {
|
} finally {
|
||||||
userState.delete.loading = false;
|
roleState.delete.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultRole },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/role/${id}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
name: data.name,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading role:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = templateRole.safeParse(roleState.update.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleState.update.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/role/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: this.form.name,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 role");
|
||||||
|
await roleState.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update data role");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating data role:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update data role"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
roleState.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
roleState.update.id = "";
|
||||||
|
roleState.update.form = { ...defaultRole };
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default userState;
|
const user = proxy({
|
||||||
|
userState,
|
||||||
|
roleState,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default user
|
||||||
@@ -126,7 +126,7 @@ export default function EditPegawai() {
|
|||||||
const success = await stateOrganisasi.edit.submit();
|
const success = await stateOrganisasi.edit.submit();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
|
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pegawai:", error);
|
console.error("Error updating pegawai:", error);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function DetailPegawai() {
|
|||||||
statePegawai.delete.byId(selectedId)
|
statePegawai.delete.byId(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
|
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ function DetailPegawai() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statePegawai.findUnique.data) {
|
if (statePegawai.findUnique.data) {
|
||||||
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
|
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!statePegawai.findUnique.data}
|
disabled={!statePegawai.findUnique.data}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function CreatePegawai() {
|
|||||||
// Reset form dan redirect
|
// Reset form dan redirect
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success("Data pegawai berhasil ditambahkan");
|
toast.success("Data pegawai berhasil ditambahkan");
|
||||||
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
|
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating pegawai:", error);
|
console.error("Error creating pegawai:", error);
|
||||||
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function ListPegawai({ search }: { search: string }) {
|
|||||||
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
||||||
<JudulList
|
<JudulList
|
||||||
title='List Pegawai'
|
title='List Pegawai'
|
||||||
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
|
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/create'
|
||||||
/>
|
/>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
@@ -131,7 +131,7 @@ function ListPegawai({ search }: { search: string }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
|
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${item.id}`)}>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function EditPosisiOrganisasi() {
|
|||||||
const success = await stateOrganisasi.edit.update();
|
const success = await stateOrganisasi.edit.update();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Error updating posisi organisasi:", error);
|
console.error("Error updating posisi organisasi:", error);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function CreatePosisiOrganisasi() {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await stateOrganisasi.create.submit();
|
await stateOrganisasi.create.submit();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi")
|
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi")
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
|
|||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<JudulList
|
<JudulList
|
||||||
title='List Posisi Organisasi'
|
title='List Posisi Organisasi'
|
||||||
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
|
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/create'
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
@@ -91,7 +91,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
|
|||||||
<Button color="green"
|
<Button color="green"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item) {
|
if (item) {
|
||||||
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`);
|
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/${item.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
title='List Kategori Kegiatan'
|
title='List Kategori Kegiatan'
|
||||||
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
||||||
/>
|
/>
|
||||||
|
<Box style={{overflowY: "auto"}}>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
@@ -97,6 +98,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
|
|||||||
@@ -68,35 +68,37 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
title='List Kategori Prestasi Desa'
|
title='List Kategori Prestasi Desa'
|
||||||
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
|
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Box style={{ overflowY: "auto" }}>
|
||||||
<TableThead>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableTr>
|
<TableThead>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTr>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh>Nama Kategori</TableTh>
|
||||||
<TableTh>Delete</TableTh>
|
<TableTh>Edit</TableTh>
|
||||||
</TableTr>
|
<TableTh>Delete</TableTh>
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button color="red" onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setModalHapus(true)
|
|
||||||
}}>
|
|
||||||
<IconX size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="red" onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconX size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
title='List Media Sosial'
|
title='List Media Sosial'
|
||||||
href='/admin/landing-page/profile/media-sosial/create'
|
href='/admin/landing-page/profile/media-sosial/create'
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Box style={{overflowY: "auto"}}>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
|
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
|
||||||
@@ -85,6 +86,7 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,30 +58,32 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
title='List Program Inovasi'
|
title='List Program Inovasi'
|
||||||
href='/admin/landing-page/profile/program-inovasi/create'
|
href='/admin/landing-page/profile/program-inovasi/create'
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Box style={{ overflowY: "auto" }}>
|
||||||
<TableThead>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableTr>
|
<TableThead>
|
||||||
<TableTh>Nama Program</TableTh>
|
<TableTr>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh>Nama Program</TableTh>
|
||||||
<TableTh>Link</TableTh>
|
<TableTh>Deskripsi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh>Link</TableTh>
|
||||||
</TableTr>
|
<TableTh>Detail</TableTh>
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>{item.description}</TableTd>
|
|
||||||
<TableTd>{item.link}</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
|
|
||||||
<IconDeviceImac size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>{item.description}</TableTd>
|
||||||
|
<TableTd>{item.link}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
'use client'
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Box,
|
|
||||||
Button, Center, FileInput, Group, Image, Paper,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
|
|
||||||
|
|
||||||
function EditStrukturPPID() {
|
|
||||||
const strukturPPID = useProxy(stateStrukturPPID);
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadData = async () => {
|
|
||||||
const id = params?.id as string;
|
|
||||||
if (!id) {
|
|
||||||
toast.error("ID tidak valid");
|
|
||||||
router.push("/admin/ppid/struktur-ppid");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const profileData = await strukturPPID.loadForEdit(id);
|
|
||||||
|
|
||||||
if (profileData && profileData.image?.link) {
|
|
||||||
setPreviewImage(profileData.image.link);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading profile:", error);
|
|
||||||
toast.error("Gagal memuat data profile");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadData();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
strukturPPID.editStruktur.reset(); // cleanup form
|
|
||||||
};
|
|
||||||
}, [params?.id, router]);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: string) => {
|
|
||||||
strukturPPID.editStruktur.updateField(field as any, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (newFile: File | null) => {
|
|
||||||
if (!newFile) {
|
|
||||||
setFile(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFile(newFile);
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
setPreviewImage(event.target?.result as string);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(newFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (isSubmitting || !strukturPPID.editStruktur.form.name.trim()) {
|
|
||||||
toast.error("Nama wajib diisi");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Upload file jika ada
|
|
||||||
if (file) {
|
|
||||||
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
|
||||||
const uploaded = uploadResponse.data?.data;
|
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
toast.error("Gagal upload gambar");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
strukturPPID.editStruktur.form.imageId = uploaded.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit form
|
|
||||||
const success = await strukturPPID.editStruktur.submit();
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success("Berhasil menyimpan perubahan");
|
|
||||||
router.push("/admin/ppid/struktur-ppid");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error submitting form:", error);
|
|
||||||
toast.error("Gagal menyimpan profile");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (strukturPPID.struktur.loading) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Center h={400}>
|
|
||||||
<Text>Memuat data struktur...</Text>
|
|
||||||
</Center>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error state
|
|
||||||
if (strukturPPID.struktur.error) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
|
||||||
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
|
||||||
<Text fw="bold">Error</Text>
|
|
||||||
<Text>{strukturPPID.struktur.error}</Text>
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No data state
|
|
||||||
if (!strukturPPID.struktur.data) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
|
||||||
<Alert icon={<IconAlertCircle size={16} />} color="yellow">
|
|
||||||
<Text fw="bold">Data tidak ditemukan</Text>
|
|
||||||
<Text>Profile PPID tidak dapat ditemukan</Text>
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack gap={'xs'}>
|
|
||||||
<Box>
|
|
||||||
<Button
|
|
||||||
variant={'subtle'}
|
|
||||||
onClick={handleBack}
|
|
||||||
>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
|
||||||
<Title order={3}>Edit Struktur PPID</Title>
|
|
||||||
<TextInput
|
|
||||||
value={strukturPPID.editStruktur.form.name}
|
|
||||||
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
|
||||||
label={<Text fz="sm" fw="bold">Judul</Text>}
|
|
||||||
placeholder="Masukkan judul"
|
|
||||||
/>
|
|
||||||
{/* File Upload */}
|
|
||||||
<FileInput
|
|
||||||
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
|
|
||||||
value={file}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Preview Gambar */}
|
|
||||||
<Box>
|
|
||||||
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
|
||||||
{previewImage ? (
|
|
||||||
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg="gray.2">
|
|
||||||
<Stack align="center" gap="xs">
|
|
||||||
<IconImageInPicture size={48} color="gray" />
|
|
||||||
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting || strukturPPID.editStruktur.loading}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditStrukturPPID;
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "Pegawai",
|
||||||
|
value: "pegawai",
|
||||||
|
href: "/admin/ppid/struktur-ppid/pegawai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Posisi Organisasi",
|
||||||
|
value: "posisiorganisasi",
|
||||||
|
href: "/admin/ppid/struktur-ppid/posisi-organisasi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Struktur Organisasi",
|
||||||
|
value: "strukturorganisasi",
|
||||||
|
href: "/admin/ppid/struktur-ppid/struktur-organisasi"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
const tab = tabs.find(t => t.value === value)
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab.href)
|
||||||
|
}
|
||||||
|
setActiveTab(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const match = tabs.find(tab => tab.href === pathname)
|
||||||
|
if (match) {
|
||||||
|
setActiveTab(match.value)
|
||||||
|
}
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>Struktur PPID Desa Darmasaba</Title>
|
||||||
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsPanel key={i} value={e.value}>
|
||||||
|
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||||
|
<></>
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutTabs;
|
||||||
12
src/app/admin/(dashboard)/ppid/struktur-ppid/layout.tsx
Normal file
12
src/app/admin/(dashboard)/ppid/struktur-ppid/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import LayoutTabs from "./_lib/layoutTabs"
|
||||||
|
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<LayoutTabs>
|
||||||
|
{children}
|
||||||
|
</LayoutTabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import stateStrukturPPID from '../../_state/ppid/struktur_ppid/struktur_PPID';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
|
|
||||||
function Page() {
|
|
||||||
const router = useRouter()
|
|
||||||
const strukturPPID = useProxy(stateStrukturPPID)
|
|
||||||
useShallowEffect(() => {
|
|
||||||
strukturPPID.struktur.load("1")
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!strukturPPID.struktur.data) {
|
|
||||||
return <Stack>
|
|
||||||
<Skeleton radius={10} h={800} />
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataArray = Array.isArray(strukturPPID.struktur.data)
|
|
||||||
? strukturPPID.struktur.data
|
|
||||||
: [strukturPPID.struktur.data];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
|
||||||
<Title order={3}>Preview Struktur PPID</Title>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
|
||||||
<Button onClick={() => router.push(`/admin/ppid/struktur-ppid/${strukturPPID.struktur.data?.id}`)} bg={colors['blue-button']}>
|
|
||||||
<IconEdit size={16} />
|
|
||||||
</Button>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
{dataArray.map((item) => (
|
|
||||||
<Paper key={item.id} p={"xl"} bg={colors['BG-trans']}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"h4"}>{item.name}</Text>
|
|
||||||
<Image
|
|
||||||
w={{ base: "100%", md: "100%" }}
|
|
||||||
src={item.image?.link}
|
|
||||||
alt=''
|
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.src = "/struktur_ppid.png";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
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 { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
export default function EditPegawaiPPID() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
namaLengkap: stateOrganisasi.edit.form.namaLengkap,
|
||||||
|
gelarAkademik: stateOrganisasi.edit.form.gelarAkademik,
|
||||||
|
imageId: stateOrganisasi.edit.form.imageId,
|
||||||
|
tanggalMasuk: stateOrganisasi.edit.form.tanggalMasuk,
|
||||||
|
email: stateOrganisasi.edit.form.email,
|
||||||
|
telepon: stateOrganisasi.edit.form.telepon,
|
||||||
|
alamat: stateOrganisasi.edit.form.alamat,
|
||||||
|
posisiId: stateOrganisasi.edit.form.posisiId,
|
||||||
|
isActive: stateOrganisasi.edit.form.isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Format date to YYYY-MM-DD for date input
|
||||||
|
const formatDateForInput = (dateString: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||||
|
const loadPegawai = async () => {
|
||||||
|
try {
|
||||||
|
const data = await stateOrganisasi.edit.load(id);
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
namaLengkap: data.namaLengkap || "",
|
||||||
|
gelarAkademik: data.gelarAkademik || "",
|
||||||
|
imageId: data.imageId || "",
|
||||||
|
tanggalMasuk: data.tanggalMasuk || "",
|
||||||
|
email: data.email || "",
|
||||||
|
telepon: data.telepon || "",
|
||||||
|
alamat: data.alamat || "",
|
||||||
|
posisiId: data.posisiId || "",
|
||||||
|
isActive: data.isActive ?? true, // pakai nullish coalescing
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.image?.link) {
|
||||||
|
setPreviewImage(data.image.link);
|
||||||
|
} else {
|
||||||
|
setPreviewImage(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal mengambil data pegawai"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPegawai();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!formData.namaLengkap.trim()) {
|
||||||
|
toast.error('Nama lengkap tidak boleh kosong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateOrganisasi.edit.form = {
|
||||||
|
namaLengkap: formData.namaLengkap.trim(),
|
||||||
|
gelarAkademik: formData.gelarAkademik.trim(),
|
||||||
|
imageId: formData.imageId || "",
|
||||||
|
tanggalMasuk: formData.tanggalMasuk.trim(),
|
||||||
|
email: formData.email.trim(),
|
||||||
|
telepon: formData.telepon.trim(),
|
||||||
|
alamat: formData.alamat.trim(),
|
||||||
|
posisiId: formData.posisiId.trim(),
|
||||||
|
isActive: formData.isActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update imageId in global state
|
||||||
|
stateOrganisasi.edit.form.imageId = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id && !stateOrganisasi.edit.id) {
|
||||||
|
stateOrganisasi.edit.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await stateOrganisasi.edit.submit();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
router.push("/admin/ppid/struktur-ppid/pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating pegawai:", error);
|
||||||
|
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 PPID</Title>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Masukkan nama lengkap"
|
||||||
|
value={formData.namaLengkap}
|
||||||
|
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Gelar Akademik"
|
||||||
|
placeholder="Contoh: S.Kom"
|
||||||
|
value={formData.gelarAkademik}
|
||||||
|
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box >
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const file = files[0]; // Hanya ambil file pertama
|
||||||
|
if (file) {
|
||||||
|
setFile(file);
|
||||||
|
setPreviewImage(URL.createObjectURL(file)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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 && (
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<TextInput
|
||||||
|
label="Tanggal Masuk"
|
||||||
|
type="date"
|
||||||
|
placeholder="Contoh: 2022-01-01"
|
||||||
|
value={formatDateForInput(formData.tanggalMasuk)}
|
||||||
|
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="Contoh: email@example.com"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => (formData.email = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Telepon"
|
||||||
|
placeholder="Contoh: 08123456789"
|
||||||
|
value={formData.telepon}
|
||||||
|
onChange={(e) => (formData.telepon = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Alamat"
|
||||||
|
placeholder="Contoh: Jl. Contoh No. 1"
|
||||||
|
value={formData.alamat}
|
||||||
|
onChange={(e) => (formData.alamat = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Posisi"
|
||||||
|
placeholder="Pilih posisi"
|
||||||
|
data={
|
||||||
|
stateStrukturPPID.posisiOrganisasi.findMany.data?.map((p) => ({
|
||||||
|
value: p.id, // harus string
|
||||||
|
label: p.nama,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
value={formData.posisiId}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value !== null) {
|
||||||
|
setFormData({ ...formData, posisiId: value }); // value harus string
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Status Pegawai"
|
||||||
|
data={[
|
||||||
|
{ value: 'true', label: 'Aktif' },
|
||||||
|
{ value: 'false', label: 'Tidak Aktif' },
|
||||||
|
]}
|
||||||
|
value={String(formData.isActive)} // 'true' atau 'false'
|
||||||
|
onChange={(val) => {
|
||||||
|
setFormData({ ...formData, isActive: val === 'true' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box >
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
'use client'
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailPegawai() {
|
||||||
|
const statePegawai = useProxy(stateStrukturPPID.pegawai)
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const params = useParams()
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
statePegawai.findUnique.load(params?.id as string)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
statePegawai.delete.byId(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
router.push("/admin/ppid/struktur-ppid/pegawai")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statePegawai.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<Text fz={"xl"} fw={"bold"}>Detail Pegawai PPID</Text>
|
||||||
|
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<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='' />
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"lg"} fw={"bold"}>Email</Text>
|
||||||
|
<Text fz={"lg"}>{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"}>{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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!statePegawai.findUnique.data}
|
||||||
|
color="red">
|
||||||
|
<IconX size={20} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (statePegawai.findUnique.data) {
|
||||||
|
router.push(`/admin/ppid/struktur-ppid/pegawai/${statePegawai.findUnique.data.id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!statePegawai.findUnique.data}
|
||||||
|
color="green">
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah anda yakin ingin menghapus pegawai ini?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPegawai;
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
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 { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreatePegawaiPPID() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||||
|
useEffect(() => {
|
||||||
|
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||||
|
resetForm();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
stateOrganisasi.create.form = {
|
||||||
|
namaLengkap: "",
|
||||||
|
gelarAkademik: "",
|
||||||
|
imageId: "",
|
||||||
|
tanggalMasuk: "",
|
||||||
|
email: "",
|
||||||
|
telepon: "",
|
||||||
|
alamat: "",
|
||||||
|
posisiId: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!previewImage) {
|
||||||
|
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload gambar dulu
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file: previewImage.file,
|
||||||
|
name: previewImage.file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error("Gagal upload gambar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set status aktif secara otomatis
|
||||||
|
stateOrganisasi.create.form.isActive = true;
|
||||||
|
|
||||||
|
// Simpan ID gambar ke form
|
||||||
|
stateOrganisasi.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
await stateOrganisasi.create.submit();
|
||||||
|
|
||||||
|
|
||||||
|
// Reset form dan redirect
|
||||||
|
resetForm();
|
||||||
|
toast.success("Data pegawai berhasil ditambahkan");
|
||||||
|
router.push("/admin/ppid/struktur-ppid/pegawai");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating pegawai:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
|
value={stateOrganisasi.create.form.namaLengkap}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Gelar Akademik"
|
||||||
|
placeholder="Contoh: S.Kom"
|
||||||
|
value={stateOrganisasi.create.form.gelarAkademik}
|
||||||
|
onChange={(e) => (stateOrganisasi.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>
|
||||||
|
|
||||||
|
<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 && (
|
||||||
|
<Image
|
||||||
|
src={previewImage.preview}
|
||||||
|
alt="Preview"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<TextInput
|
||||||
|
label="Tanggal Masuk"
|
||||||
|
type="date"
|
||||||
|
placeholder="Contoh: 2022-01-01"
|
||||||
|
value={stateOrganisasi.create.form.tanggalMasuk}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="Contoh: email@example.com"
|
||||||
|
value={stateOrganisasi.create.form.email}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Telepon"
|
||||||
|
placeholder="Contoh: 08123456789"
|
||||||
|
value={stateOrganisasi.create.form.telepon}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Alamat"
|
||||||
|
placeholder="Contoh: Jl. Contoh No. 1"
|
||||||
|
value={stateOrganisasi.create.form.alamat}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Posisi"
|
||||||
|
placeholder="Pilih posisi"
|
||||||
|
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
|
||||||
|
value: p.id,
|
||||||
|
label: p.nama
|
||||||
|
})) || []}
|
||||||
|
value={stateOrganisasi.create.form.posisiId}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value) stateOrganisasi.create.form.posisiId = value;
|
||||||
|
}}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePegawaiPPID;
|
||||||
178
src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/page.tsx
Normal file
178
src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/page.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/* 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 { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
|
||||||
|
function PegawaiPPID() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Pegawai PPID'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListPegawaiPPID search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListPegawaiPPID({ search }: { search: string }) {
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = stateOrganisasi.findMany;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(page, 10);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||||
|
}, [data, search]);
|
||||||
|
|
||||||
|
// Handle loading state
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={300} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<JudulList
|
||||||
|
title='List Pegawai'
|
||||||
|
href='/admin/ppid/struktur-ppid/pegawai/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Gelar Akademik</TableTh>
|
||||||
|
<TableTh>Telepon</TableTh>
|
||||||
|
<TableTh>Posisi</TableTh>
|
||||||
|
<TableTh>Aktif</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
||||||
|
<JudulList
|
||||||
|
title='List Pegawai'
|
||||||
|
href='/admin/ppid/struktur-ppid/pegawai/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Gelar Akademik</TableTh>
|
||||||
|
<TableTh>Telepon</TableTh>
|
||||||
|
<TableTh>Posisi</TableTh>
|
||||||
|
<TableTh>Aktif</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{(() => {
|
||||||
|
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
{([...filteredData]
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.isActive === b.isActive) {
|
||||||
|
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
|
||||||
|
}
|
||||||
|
return Number(b.isActive) - Number(a.isActive); // aktif duluan
|
||||||
|
}) // Aktif di atas
|
||||||
|
).map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.namaLengkap}</TableTd>
|
||||||
|
<TableTd>{item.gelarAkademik}</TableTd>
|
||||||
|
<TableTd>{item.telepon}</TableTd>
|
||||||
|
<TableTd>{item.posisi?.nama}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Box visibleFrom="sm">
|
||||||
|
<Badge color={item.isActive ? "green" : "red"}>
|
||||||
|
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box hiddenFrom="sm">
|
||||||
|
{item.isActive ? (
|
||||||
|
<ThemeIcon color="green" variant="light" size="sm">
|
||||||
|
<IconCheck size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
) : (
|
||||||
|
<ThemeIcon color="red" variant="light" size="sm">
|
||||||
|
<IconX size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}>
|
||||||
|
<IconDeviceImacCog size={25} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PegawaiPPID;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function EditPosisiOrganisasiPPID() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nama: "",
|
||||||
|
deskripsi: "",
|
||||||
|
hierarki: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPosisiOrganisasi = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await stateOrganisasi.edit.load(id);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// pastikan id-nya masuk ke state edit
|
||||||
|
stateOrganisasi.edit.id = id;
|
||||||
|
setFormData({
|
||||||
|
nama: data.nama || '',
|
||||||
|
deskripsi: data.deskripsi || '',
|
||||||
|
hierarki: data.hierarki || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading posisi organisasi:", error);
|
||||||
|
toast.error("Gagal memuat data posisi organisasi");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPosisiOrganisasi();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!formData.nama.trim()) {
|
||||||
|
toast.error('Nama posisi organisasi tidak boleh kosong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateOrganisasi.edit.form = {
|
||||||
|
nama: formData.nama.trim(),
|
||||||
|
deskripsi: formData.deskripsi.trim(),
|
||||||
|
hierarki: formData.hierarki,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safety check tambahan: pastikan ID tidak kosong
|
||||||
|
if (!stateOrganisasi.edit.id) {
|
||||||
|
stateOrganisasi.edit.id = id; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await stateOrganisasi.edit.update();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
router.push("/admin/ppid/struktur-ppid/posisi-organisasi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating posisi organisasi:", error);
|
||||||
|
// toast akan ditampilkan dari fungsi update
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Posisi Organisasi PPID</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.nama}
|
||||||
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
|
||||||
|
placeholder='Masukkan nama posisi organisasi'
|
||||||
|
/>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
setFormData({ ...formData, deskripsi: htmlContent });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={formData.hierarki}
|
||||||
|
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
|
||||||
|
placeholder='Masukkan hierarki'
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPosisiOrganisasiPPID;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreatePosisiOrganisasiPPID() {
|
||||||
|
const router = useRouter();
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
|
||||||
|
useEffect(() => {
|
||||||
|
stateOrganisasi.findMany.load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
stateOrganisasi.create.form = {
|
||||||
|
nama: "",
|
||||||
|
deskripsi: "",
|
||||||
|
hierarki: 0, // Initialize as 0 to allow any number input
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await stateOrganisasi.create.submit();
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/ppid/struktur-ppid/posisi-organisasi")
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Posisi Organisasi PPID</Title>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Posisi"
|
||||||
|
placeholder="Contoh: Kepala Desa"
|
||||||
|
value={stateOrganisasi.create.form.nama}
|
||||||
|
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<CreateEditor
|
||||||
|
value={stateOrganisasi.create.form.deskripsi}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
stateOrganisasi.create.form.deskripsi = htmlContent;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Hierarki"
|
||||||
|
type="number"
|
||||||
|
placeholder="Contoh: 1"
|
||||||
|
value={stateOrganisasi.create.form.hierarki}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.currentTarget.value, 10);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
stateOrganisasi.create.form.hierarki = value;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePosisiOrganisasiPPID;
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
|
||||||
|
function PosisiOrganisasiPPID() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Posisi Organisasi PPID'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListPosisiOrganisasiPPID search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
|
||||||
|
const router = useRouter();
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateOrganisasi.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleHapus = async () => {
|
||||||
|
if (selectedId) {
|
||||||
|
await stateOrganisasi.delete.byId(selectedId);
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.nama?.toLowerCase().includes(keyword) ||
|
||||||
|
item.deskripsi?.toLowerCase().includes(keyword) ||
|
||||||
|
item.hierarki?.toString().toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.hierarki - b.hierarki);
|
||||||
|
|
||||||
|
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 Posisi Organisasi PPID'
|
||||||
|
href='/admin/ppid/struktur-ppid/posisi-organisasi/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowY: 'auto', maxHeight: '400px' }}>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama Posisi</TableTh>
|
||||||
|
<TableTh>Deskripsi</TableTh>
|
||||||
|
<TableTh>Hierarki</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody >
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{item.nama}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text truncate dangerouslySetInnerHTML={{ __html: item.deskripsi ?? "" }} />
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.hierarki}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="green"
|
||||||
|
onClick={() => {
|
||||||
|
if (item) {
|
||||||
|
router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color="red"
|
||||||
|
onClick={() => {
|
||||||
|
if (item) {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!item}
|
||||||
|
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
{/* Modal Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah anda yakin ingin menghapus posisi organisasi PPID ini?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PosisiOrganisasiPPID;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* eslint-disable prefer-const */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { OrganizationChart } from 'primereact/organizationchart';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
|
||||||
|
function StrukturOrganisasiPPID() {
|
||||||
|
return (
|
||||||
|
<Box >
|
||||||
|
<ListStrukturOrganisasiPPID />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListStrukturOrganisasiPPID() {
|
||||||
|
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateOrganisasi.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Group pegawai berdasarkan posisiId
|
||||||
|
const posisiMap = new Map<string, any>();
|
||||||
|
|
||||||
|
for (const pegawai of stateOrganisasi.findMany.data) {
|
||||||
|
const posisiId = pegawai.posisi.id;
|
||||||
|
if (!posisiMap.has(posisiId)) {
|
||||||
|
posisiMap.set(posisiId, {
|
||||||
|
...pegawai.posisi,
|
||||||
|
pegawaiList: [],
|
||||||
|
children: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Step 2: Buat struktur pohon berdasarkan parentId
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Ubah struktur ke format OrganizationChart
|
||||||
|
function toOrgChartFormat(node: any): any {
|
||||||
|
return {
|
||||||
|
expanded: true,
|
||||||
|
type: 'person',
|
||||||
|
styleClass: 'p-person',
|
||||||
|
data: {
|
||||||
|
name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak 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 bg={colors.grey} p="md" style={{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 status = node?.data?.status || 'Tidak ada deskripsi';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="center" gap={4}>
|
||||||
|
<Image
|
||||||
|
src={imageSrc}
|
||||||
|
alt={name}
|
||||||
|
radius="xl"
|
||||||
|
w={120}
|
||||||
|
h={120}
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<Text fw={600} ta="center">{name}</Text>
|
||||||
|
<Text size="sm" c="dimmed" ta="center">{status}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default StrukturOrganisasiPPID;
|
||||||
@@ -49,7 +49,7 @@ export const navBar = [
|
|||||||
{
|
{
|
||||||
id: "PPID_2",
|
id: "PPID_2",
|
||||||
name: "Struktur PPID",
|
name: "Struktur PPID",
|
||||||
path: "/admin/ppid/struktur-ppid"
|
path: "/admin/ppid/struktur-ppid/pegawai"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PPID_3",
|
id: "PPID_3",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const Pegawai = new Elysia({
|
|||||||
id: t.String(),
|
id: t.String(),
|
||||||
namaLengkap: t.Optional(t.String()),
|
namaLengkap: t.Optional(t.String()),
|
||||||
gelarAkademik: t.Optional(t.String()),
|
gelarAkademik: t.Optional(t.String()),
|
||||||
imageId: t.String(),
|
imageId: t.Optional(t.String()),
|
||||||
tanggalMasuk: t.Optional(t.String()),
|
tanggalMasuk: t.Optional(t.String()),
|
||||||
email: t.Optional(t.String()),
|
email: t.Optional(t.String()),
|
||||||
telepon: t.Optional(t.String()),
|
telepon: t.Optional(t.String()),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ type FormUpdatePegawai = {
|
|||||||
id: string;
|
id: string;
|
||||||
namaLengkap?: string;
|
namaLengkap?: string;
|
||||||
gelarAkademik?: string;
|
gelarAkademik?: string;
|
||||||
imageId: string;
|
imageId?: string;
|
||||||
tanggalMasuk?: string;
|
tanggalMasuk?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
telepon?: string;
|
telepon?: string;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import strukturPPIDFindById from "./find-by-id";
|
import strukturPPIDFindById from "./find-by-id";
|
||||||
import strukturPPIDUpdate from "./update";
|
import strukturPPIDUpdate from "./update";
|
||||||
|
import Pegawai from "./pegawai";
|
||||||
|
import PosisiOrganisasi from "./posisi-organisasi";
|
||||||
|
|
||||||
const StrukturPPID = new Elysia({
|
const StrukturPPID = new Elysia({
|
||||||
prefix: "/strukturppid",
|
prefix: "/strukturppid",
|
||||||
@@ -10,6 +12,8 @@ const StrukturPPID = new Elysia({
|
|||||||
const response = await strukturPPIDFindById(new Request(context.request))
|
const response = await strukturPPIDFindById(new Request(context.request))
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
.use(Pegawai)
|
||||||
|
.use(PosisiOrganisasi)
|
||||||
.put("/:id", async (context) => {
|
.put("/:id", async (context) => {
|
||||||
const response = await strukturPPIDUpdate(context)
|
const response = await strukturPPIDUpdate(context)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreatePegawai = {
|
||||||
|
namaLengkap: string;
|
||||||
|
gelarAkademik?: string;
|
||||||
|
imageId: string;
|
||||||
|
tanggalMasuk?: string; // Kirim dari frontend dalam format ISO (ex: '2025-07-04')
|
||||||
|
email?: string;
|
||||||
|
telepon?: string;
|
||||||
|
alamat?: string;
|
||||||
|
posisiId: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function pegawaiCreate(context: Context) {
|
||||||
|
const body = await context.body as FormCreatePegawai;
|
||||||
|
|
||||||
|
if (!body || typeof body !== 'object') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Data tidak valid",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.namaLengkap || !body.posisiId || !body.imageId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Nama lengkap, posisi, dan gambar wajib diisi",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pegawai = await prisma.pegawaiPPID.create({
|
||||||
|
data: {
|
||||||
|
namaLengkap: body.namaLengkap,
|
||||||
|
gelarAkademik: body.gelarAkademik,
|
||||||
|
imageId: body.imageId,
|
||||||
|
tanggalMasuk: body.tanggalMasuk ? new Date(body.tanggalMasuk) : undefined,
|
||||||
|
email: body.email,
|
||||||
|
telepon: body.telepon,
|
||||||
|
alamat: body.alamat,
|
||||||
|
posisiId: body.posisiId,
|
||||||
|
isActive: body.isActive ?? true, // default true kalau tidak dikirim
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil menambahkan pegawai",
|
||||||
|
data: pegawai,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error creating pegawai:", error);
|
||||||
|
|
||||||
|
if (error.code === "P2002") {
|
||||||
|
// Cek field mana yang duplicate
|
||||||
|
const target = error.meta?.target?.[0];
|
||||||
|
let message = "Data sudah ada";
|
||||||
|
|
||||||
|
if (target === 'email') message = "Email sudah digunakan";
|
||||||
|
else if (target === 'telepon') message = "Nomor telepon sudah digunakan";
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Gagal menambahkan pegawai:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat membuat pegawai",
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function pegawaiDelete(context: Context) {
|
||||||
|
const { id } = context.params as { id: string };
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID pegawai tidak ditemukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleted = await prisma.pegawaiPPID.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
isActive: false, // soft delete
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Pegawai berhasil di-nonaktifkan",
|
||||||
|
data: deleted,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error delete pegawai:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal menghapus pegawai",
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
// Di findMany.ts
|
||||||
|
export default async function pegawaiFindMany(context: Context) {
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.pegawaiPPID.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: {
|
||||||
|
posisi: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
prisma.pegawaiPPID.count({
|
||||||
|
where: { isActive: true }
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch pegawai with pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Find many paginated error:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed fetch pegawai with pagination",
|
||||||
|
data: [],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function pegawaiFindUnique(context: Context) {
|
||||||
|
const { id } = context.params as { id: string };
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID pegawai diperlukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pegawai = await prisma.pegawaiPPID.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
posisi: true,
|
||||||
|
image: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pegawai) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Pegawai tidak ditemukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: pegawai,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error findUnique pegawai:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data pegawai",
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import pegawaiFindMany from "./findMany";
|
||||||
|
import pegawaiFindUnique from "./findUnique";
|
||||||
|
import pegawaiCreate from "./create";
|
||||||
|
import pegawaiDelete from "./del";
|
||||||
|
import pegawaiUpdate from "./updt";
|
||||||
|
|
||||||
|
|
||||||
|
const Pegawai = new Elysia({
|
||||||
|
prefix: "/pegawai",
|
||||||
|
tags: ["PPID/Struktur PPID/Pegawai"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Find all
|
||||||
|
.get("/find-many", pegawaiFindMany)
|
||||||
|
|
||||||
|
// ✅ Find by ID
|
||||||
|
.get("/:id", async (context) => {
|
||||||
|
const response = await pegawaiFindUnique(context);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Create
|
||||||
|
.post("/create", pegawaiCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
namaLengkap: t.String(),
|
||||||
|
gelarAkademik: t.Optional(t.String()),
|
||||||
|
imageId: t.String(),
|
||||||
|
tanggalMasuk: t.Optional(t.String()), // ISO string (YYYY-MM-DD)
|
||||||
|
email: t.Optional(t.String()),
|
||||||
|
telepon: t.Optional(t.String()),
|
||||||
|
alamat: t.Optional(t.String()),
|
||||||
|
posisiId: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Update
|
||||||
|
.put(
|
||||||
|
"/:id",
|
||||||
|
async (context) => {
|
||||||
|
const response = await pegawaiUpdate(context);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
namaLengkap: t.String(),
|
||||||
|
gelarAkademik: t.String(),
|
||||||
|
imageId: t.String(),
|
||||||
|
tanggalMasuk: t.String(),
|
||||||
|
email: t.String(),
|
||||||
|
telepon: t.String(),
|
||||||
|
alamat: t.String(),
|
||||||
|
posisiId: t.String(),
|
||||||
|
isActive: t.Boolean(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ✅ Delete
|
||||||
|
.delete("/del/:id", pegawaiDelete);
|
||||||
|
|
||||||
|
export default Pegawai;
|
||||||
141
src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/pegawai/updt.ts
Normal file
141
src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/pegawai/updt.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
type FormUpdate = Prisma.PegawaiPPIDGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
namaLengkap: true;
|
||||||
|
gelarAkademik: true;
|
||||||
|
tanggalMasuk: true;
|
||||||
|
email: true;
|
||||||
|
imageId: true;
|
||||||
|
telepon: true;
|
||||||
|
alamat: true;
|
||||||
|
posisiId: true;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
export default async function pegawaiUpdate(context: Context) {
|
||||||
|
try {
|
||||||
|
const id = context.params?.id as string;
|
||||||
|
const body = (await context.body) as Omit<FormUpdate, "id">;
|
||||||
|
|
||||||
|
body.isActive = String(body.isActive).toLowerCase() === 'true';
|
||||||
|
|
||||||
|
const {
|
||||||
|
namaLengkap,
|
||||||
|
gelarAkademik,
|
||||||
|
tanggalMasuk,
|
||||||
|
email,
|
||||||
|
telepon,
|
||||||
|
imageId,
|
||||||
|
alamat,
|
||||||
|
posisiId,
|
||||||
|
} = 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.pegawaiPPID.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.pegawaiPPID.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
namaLengkap,
|
||||||
|
gelarAkademik,
|
||||||
|
tanggalMasuk,
|
||||||
|
email,
|
||||||
|
imageId,
|
||||||
|
telepon,
|
||||||
|
alamat,
|
||||||
|
posisiId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
image: true,
|
||||||
|
posisi: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Profile PPID Berhasil Dibuat",
|
||||||
|
data: updated,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating profile PPID:", error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat mengupdate profile PPID",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreate = {
|
||||||
|
nama: string;
|
||||||
|
deskripsi: string;
|
||||||
|
hierarki: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function posisiOrganisasiCreate(context: Context) {
|
||||||
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
|
if(!body) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Body is required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const posisiOrganisasi = await prisma.posisiOrganisasiPPID.create({
|
||||||
|
data: {
|
||||||
|
nama: body.nama,
|
||||||
|
deskripsi: body.deskripsi,
|
||||||
|
hierarki: body.hierarki,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success create posisi organisasi",
|
||||||
|
data: posisiOrganisasi
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating PosisiOrganisasi:", error);
|
||||||
|
throw new Error("Failed to create PosisiOrganisasi: " + (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function posisiOrganisasiDelete(context: Context) {
|
||||||
|
const { id } = context.params as { id: string };
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "ID wajib diisi",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the position exists first
|
||||||
|
const existing = await prisma.posisiOrganisasiPPID.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Posisi organisasi tidak ditemukan",
|
||||||
|
}),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are any pegawai associated with this position
|
||||||
|
const pegawaiCount = await prisma.pegawaiPPID.count({
|
||||||
|
where: { posisiId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pegawaiCount > 0) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Tidak dapat menghapus posisi yang masih memiliki pegawai",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all checks pass, delete the position
|
||||||
|
const deleted = await prisma.posisiOrganisasiPPID.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Posisi organisasi berhasil dihapus",
|
||||||
|
data: deleted,
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error delete posisi organisasi:", error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat menghapus posisi organisasi",
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function posisiOrganisasiFindMany() {
|
||||||
|
const data = await prisma.posisiOrganisasiPPID.findMany();
|
||||||
|
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function posisiOrganisasiFindUnique(context: Context) {
|
||||||
|
const url = new URL(context.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.posisiOrganisasiPPID.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Posisi organisasi tidak ditemukan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success find posisi organisasi",
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find by ID error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil posisi organisasi: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import posisiOrganisasiFindMany from "./findMany";
|
||||||
|
import posisiOrganisasiFindUnique from "./findUnique";
|
||||||
|
import posisiOrganisasiCreate from "./create";
|
||||||
|
import posisiOrganisasiUpdate from "./updt";
|
||||||
|
import posisiOrganisasiDelete from "./del";
|
||||||
|
|
||||||
|
const PosisiOrganisasi = new Elysia({
|
||||||
|
prefix: "/posisiorganisasi",
|
||||||
|
tags: ["PPID/Struktur PPID/Posisi Organisasi"],
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/find-many", posisiOrganisasiFindMany)
|
||||||
|
.get("/:id", async (context) => {
|
||||||
|
const response = await posisiOrganisasiFindUnique(context);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.post("/create", posisiOrganisasiCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
nama: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
hierarki: t.Number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.put("/:id", async (context) => {
|
||||||
|
const response = await posisiOrganisasiUpdate(context);
|
||||||
|
return response;
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
nama: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
hierarki: t.Number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.delete("/del/:id", posisiOrganisasiDelete);
|
||||||
|
|
||||||
|
export default PosisiOrganisasi;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormUpdate = {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
deskripsi: string;
|
||||||
|
hierarki: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function posisiOrganisasiUpdate(context: Context) {
|
||||||
|
const body = context.body as FormUpdate;
|
||||||
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "ID is required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.posisiOrganisasiPPID.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
nama: body.nama,
|
||||||
|
deskripsi: body.deskripsi,
|
||||||
|
hierarki: body.hierarki,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await prisma.posisiOrganisasiPPID.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success update posisi organisasi",
|
||||||
|
data: updated,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal update posisi organisasi",
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,42 +2,87 @@ import prisma from "@/lib/prisma";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
interface RegisterBody {
|
||||||
|
nama: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function userRegister(context: Context) {
|
export default async function userRegister(context: Context) {
|
||||||
const body = (await context.body) as {
|
try {
|
||||||
nama: string;
|
const body = (await context.body) as RegisterBody;
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingUser = await prisma.user.findUnique({
|
// Validasi input
|
||||||
where: { email: body.email },
|
if (!body.nama || !body.email || !body.password) {
|
||||||
});
|
context.set.status = 400;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Semua field harus diisi",
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (existingUser) {
|
// Cek email sudah terdaftar
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { email: body.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
context.set.status = 400;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Email sudah terdaftar",
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan role warga
|
||||||
|
const role = await prisma.role.findFirst({
|
||||||
|
where: { name: "warga" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
context.set.status = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Role warga tidak ditemukan",
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hashedPassword = await bcrypt.hash(body.password, 10);
|
||||||
|
|
||||||
|
// Buat user baru
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
nama: body.nama,
|
||||||
|
email: body.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
roleId: role.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
nama: true,
|
||||||
|
email: true,
|
||||||
|
roleId: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil mendaftar",
|
||||||
|
data: user,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Registration error:", error);
|
||||||
|
context.set.status = 500;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Email sudah terdaftar",
|
message: "Terjadi kesalahan saat mendaftar",
|
||||||
|
data: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = await prisma.role.findFirst({ where: { name: "warga" } });
|
|
||||||
|
|
||||||
if (!role) throw new Error("Role warga tidak ditemukan");
|
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(body.password, 10);
|
|
||||||
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
nama: body.nama,
|
|
||||||
email: body.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
roleId: role.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Berhasil daftar sebagai warga",
|
|
||||||
data: user,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/app/api/[[...slugs]]/_lib/user/role/create.ts
Normal file
26
src/app/api/[[...slugs]]/_lib/user/role/create.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormCreate = {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function roleCreate(context: Context) {
|
||||||
|
const body = (await context.body) as FormCreate;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await prisma.role.create({
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil membuat role",
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating role:", error);
|
||||||
|
throw new Error("Gagal membuat role: " + (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/api/[[...slugs]]/_lib/user/role/del.ts
Normal file
16
src/app/api/[[...slugs]]/_lib/user/role/del.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function roleDelete(context: Context) {
|
||||||
|
const id = context.params.id as string;
|
||||||
|
|
||||||
|
await prisma.role.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
success: true,
|
||||||
|
message: "Success delete role",
|
||||||
|
};
|
||||||
|
}
|
||||||
11
src/app/api/[[...slugs]]/_lib/user/role/findMany.ts
Normal file
11
src/app/api/[[...slugs]]/_lib/user/role/findMany.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function roleFindMany() {
|
||||||
|
const data = await prisma.role.findMany();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success get all role",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
46
src/app/api/[[...slugs]]/_lib/user/role/findUnique.ts
Normal file
46
src/app/api/[[...slugs]]/_lib/user/role/findUnique.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function roleFindUnique(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.role.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Data not found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success get role",
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find by ID error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/app/api/[[...slugs]]/_lib/user/role/index.ts
Normal file
33
src/app/api/[[...slugs]]/_lib/user/role/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import roleCreate from "./create";
|
||||||
|
import roleDelete from "./del";
|
||||||
|
import roleFindMany from "./findMany";
|
||||||
|
import roleFindUnique from "./findUnique";
|
||||||
|
import roleUpdate from "./updt";
|
||||||
|
|
||||||
|
const Role = new Elysia({
|
||||||
|
prefix: "/api/role",
|
||||||
|
tags: ["User / Role"],
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/create", roleCreate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/findMany", roleFindMany)
|
||||||
|
.get("/:id", async (context) => {
|
||||||
|
const response = await roleFindUnique(
|
||||||
|
new Request(context.request)
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.put("/:id", roleUpdate, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.delete("/del/:id", roleDelete);
|
||||||
|
|
||||||
|
export default Role;
|
||||||
28
src/app/api/[[...slugs]]/_lib/user/role/updt.ts
Normal file
28
src/app/api/[[...slugs]]/_lib/user/role/updt.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormUpdate = {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function roleUpdate(context: Context) {
|
||||||
|
const body = (await context.body) as FormUpdate;
|
||||||
|
const id = context.params.id as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await prisma.role.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: body.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil mengupdate role",
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating role:", error);
|
||||||
|
throw new Error("Gagal mengupdate role: " + (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import Lingkungan from "./_lib/lingkungan";
|
|||||||
import LandingPage from "./_lib/landing_page";
|
import LandingPage from "./_lib/landing_page";
|
||||||
import Pendidikan from "./_lib/pendidikan";
|
import Pendidikan from "./_lib/pendidikan";
|
||||||
import User from "./_lib/user";
|
import User from "./_lib/user";
|
||||||
|
import Role from "./_lib/user/role";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ const ApiServer = new Elysia()
|
|||||||
.use(Lingkungan)
|
.use(Lingkungan)
|
||||||
.use(Pendidikan)
|
.use(Pendidikan)
|
||||||
.use(User)
|
.use(User)
|
||||||
|
.use(Role)
|
||||||
|
|
||||||
.onError(({ code }) => {
|
.onError(({ code }) => {
|
||||||
if (code === "NOT_FOUND") {
|
if (code === "NOT_FOUND") {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function Page() {
|
|||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||||
<Box pb={30}>
|
<Box pb={30}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/api/img/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
||||||
</Center>
|
</Center>
|
||||||
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} fw={"bold"}>
|
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} fw={"bold"}>
|
||||||
MOTO PPID DESA DARMASABA
|
MOTO PPID DESA DARMASABA
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import "./globals.css";
|
|||||||
import '@mantine/charts/styles.css';
|
import '@mantine/charts/styles.css';
|
||||||
import '@mantine/dates/styles.css';
|
import '@mantine/dates/styles.css';
|
||||||
import '@mantine/tiptap/styles.css';
|
import '@mantine/tiptap/styles.css';
|
||||||
|
import "primereact/resources/themes/lara-light-blue/theme.css";
|
||||||
|
import "primereact/resources/primereact.min.css";
|
||||||
|
import "primeicons/primeicons.css";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconUserFilled } from '@tabler/icons-react';
|
import { IconUserFilled } from '@tabler/icons-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
import userState from '../admin/(dashboard)/_state/user/user-state';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
|
const router = useRouter()
|
||||||
|
const snap = useSnapshot(userState.userState)
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
router.push("/darmasaba/pendidikan/perpustakaan-digital")
|
||||||
|
await snap.login.submit()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -33,15 +43,32 @@ function Page() {
|
|||||||
<IconUserFilled size={80} color={colors['blue-button']} />
|
<IconUserFilled size={80} color={colors['blue-button']} />
|
||||||
<Box>
|
<Box>
|
||||||
<Text c={colors['blue-button']} fw={'bold'}>Masuk Untuk Akses Lebih Banyak Buku</Text>
|
<Text c={colors['blue-button']} fw={'bold'}>Masuk Untuk Akses Lebih Banyak Buku</Text>
|
||||||
<TextInput placeholder='Email' />
|
<TextInput
|
||||||
<TextInput py={20} placeholder='Password' />
|
type='email'
|
||||||
|
label='Email'
|
||||||
|
placeholder='Email'
|
||||||
|
value={snap.login.form.email}
|
||||||
|
onChange={(e) => {
|
||||||
|
userState.userState.login.form.email = e.target.value
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextInput py={20}
|
||||||
|
type='password'
|
||||||
|
label='Password'
|
||||||
|
placeholder='Password'
|
||||||
|
value={snap.login.form.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
userState.userState.login.form.password = e.target.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box pb={20} >
|
<Box pb={20} >
|
||||||
<Button component={Link} href={'/darmasaba/pendidikan/perpustakaan-digital'} fullWidth bg={colors['blue-button']} radius={'xl'}>Masuk</Button>
|
<Button onClick={handleSubmit} fullWidth bg={colors['blue-button']} radius={'xl'}>Masuk</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex justify={'center'} align={'center'}>
|
<Flex justify={'center'} align={'center'}>
|
||||||
<Text>Belum punya akun? </Text>
|
<Text>Belum punya akun? </Text>
|
||||||
<Button variant='transparent' component={Link} href={'/registrasi'}>
|
<Button variant='transparent' component={Link} href={'/registrasi'}>
|
||||||
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
|
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import Link from 'next/link';
|
|
||||||
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
import userState from '../admin/(dashboard)/_state/user/user-state';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
|
const router = useRouter()
|
||||||
|
const registrerState = useSnapshot(userState.userState)
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
router.push("/login")
|
||||||
|
await registrerState.register.submit()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -33,27 +43,37 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<TextInput placeholder='Nama Lengkap'
|
<TextInput placeholder='Nama Lengkap'
|
||||||
label='Nama Lengkap'
|
label='Nama Lengkap'
|
||||||
|
value={registrerState.register.form.nama}
|
||||||
|
onChange={(e) => {
|
||||||
|
userState.userState.register.form.nama = e.target.value
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<TextInput py={10} placeholder='Email'
|
<TextInput py={10} placeholder='Email'
|
||||||
label='Email'
|
label='Email'
|
||||||
/>
|
value={registrerState.register.form.email}
|
||||||
<TextInput placeholder='day / month / year'
|
onChange={(e) => {
|
||||||
label='Tanggal Lahir'
|
userState.userState.register.form.email = e.target.value
|
||||||
/>
|
}}
|
||||||
<TextInput py={10} placeholder='08xx-xxxx-xxxx'
|
required
|
||||||
label='Nomor Telepon'
|
|
||||||
/>
|
/>
|
||||||
<TextInput pb={10} placeholder='Password'
|
<TextInput pb={10} placeholder='Password'
|
||||||
|
type='password'
|
||||||
label='Password'
|
label='Password'
|
||||||
|
value={registrerState.register.form.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
userState.userState.register.form.password = e.target.value
|
||||||
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Box pb={10}>
|
<Box pb={10}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
||||||
label="Saya menyetujui syarat dan ketentuan yang berlaku"
|
label="Saya menyetujui syarat dan ketentuan yang berlaku"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={20} >
|
<Box pb={20} >
|
||||||
<Button component={Link} href={'/darmasaba/pendidikan/perpustakaan-digital'} fullWidth bg={colors['blue-button']} radius={'xl'}>Daftar</Button>
|
<Button onClick={handleSubmit} fullWidth bg={colors['blue-button']} radius={'xl'}>Daftar</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user